Showing preview only (1,013K chars total). The displayed content is truncated. Use the JSON API for full output.
Repository: nicejade/nicelinks-vue-client
Branch: master
Commit: 416d30ee8a9a
Files: 165
Total size: 963.1 KB
Directory structure:
gitextract_dss6p7fj/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── ads.txt
├── build/
│ ├── build.js
│ ├── check-versions.js
│ ├── deploy.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── generate-sitemap.js
│ ├── load-minified.js
│ ├── service-worker-dev.js
│ ├── service-worker-prod.js
│ ├── utils.js
│ ├── vendor-manifest.json
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ ├── webpack.dll.conf.js
│ ├── webpack.prod.conf.js
│ └── webpack.test.conf.js
├── config/
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ ├── svgo-config.json
│ └── test.env.js
├── index.ejs
├── package.json
├── src/
│ ├── App.vue
│ ├── assets/
│ │ ├── icons/
│ │ │ └── index.js
│ │ └── scss/
│ │ ├── colors.scss
│ │ ├── common.scss
│ │ ├── frame.scss
│ │ ├── icon.scss
│ │ ├── layout.scss
│ │ ├── media.scss
│ │ ├── mixins.scss
│ │ ├── style.scss
│ │ ├── theme-element.scss
│ │ └── variables.scss
│ ├── components/
│ │ ├── CountUp.vue
│ │ ├── Elevator.vue
│ │ ├── Heart.vue
│ │ ├── HeartBroken.vue
│ │ ├── Icon/
│ │ │ ├── Icon.vue
│ │ │ └── index.js
│ │ ├── LoadMore.vue
│ │ ├── OfflineSeal.vue
│ │ ├── OperateTabs.vue
│ │ ├── Pagination.vue
│ │ ├── RecommendSeal.vue
│ │ ├── Search.vue
│ │ ├── SimilarRecommend.vue
│ │ ├── SubHead.vue
│ │ ├── SwitchTheme.vue
│ │ ├── UploadAvatar.vue
│ │ ├── Waline.vue
│ │ ├── dialog/
│ │ │ ├── AdBlockDialog.vue
│ │ │ ├── AutoDialog.vue
│ │ │ ├── EditDialog.vue
│ │ │ └── SentencesDialog.vue
│ │ ├── homepage/
│ │ │ ├── HomeLotus.vue
│ │ │ ├── Introduction.vue
│ │ │ ├── LinkCountup.vue
│ │ │ └── NiceFantasy.vue
│ │ ├── linksList/
│ │ │ ├── Index.vue
│ │ │ └── LinkItem.vue
│ │ ├── markdown/
│ │ │ ├── Index.vue
│ │ │ ├── PreviewMd.vue
│ │ │ └── markdown.css
│ │ └── sidebar/
│ │ ├── AdsPosition.vue
│ │ ├── AwesomeSentence.vue
│ │ ├── FriendsLinks.vue
│ │ ├── Main.vue
│ │ └── SitesRecommend.vue
│ ├── config/
│ │ ├── about.js
│ │ ├── classify.js
│ │ ├── constant.js
│ │ ├── default.js
│ │ ├── placeholder.js
│ │ ├── sentences.js
│ │ ├── tags.js
│ │ └── theme.js
│ ├── filters.js
│ ├── global.js
│ ├── helper/
│ │ ├── ajax.js
│ │ ├── apis.js
│ │ ├── auth.js
│ │ ├── document.js
│ │ ├── errorReport.js
│ │ ├── index.js
│ │ ├── marked.js
│ │ ├── system.js
│ │ ├── tool.js
│ │ ├── uploadAvatar.js
│ │ └── util.js
│ ├── locales/
│ │ └── zh.js
│ ├── main.js
│ ├── mixins/
│ │ ├── globalMixin.js
│ │ └── partsMixin.js
│ ├── partials/
│ │ ├── FooterNav.vue
│ │ ├── Frame.vue
│ │ ├── HeaderNav.vue
│ │ ├── Login.vue
│ │ ├── NotFound.vue
│ │ └── SideNav.vue
│ ├── router/
│ │ ├── beforeEachHooks.js
│ │ ├── commonRoutes.js
│ │ ├── index.js
│ │ └── routers/
│ │ ├── index.js
│ │ ├── mainRouter.js
│ │ └── manageRouter.js
│ ├── store/
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
│ └── views/
│ ├── About.vue
│ ├── Account.vue
│ ├── Business.vue
│ ├── Cemetery.vue
│ ├── ForgotPwd.vue
│ ├── FriendLink.vue
│ ├── Homepage.vue
│ ├── Index.vue
│ ├── Nicelinks.vue
│ ├── Post.vue
│ ├── Recommend.vue
│ ├── Redirect.vue
│ ├── Setting.vue
│ ├── Sponsor.vue
│ ├── Tags.vue
│ ├── TagsCollections.vue
│ ├── Theme.vue
│ ├── ThemeCollections.vue
│ ├── manage/
│ │ ├── Adverts.vue
│ │ ├── Friends.vue
│ │ ├── Index.vue
│ │ ├── Links.vue
│ │ ├── Sentences.vue
│ │ └── Users.vue
│ └── share/
│ └── ShareLink.vue
├── static/
│ ├── .gitkeep
│ ├── css/
│ │ └── app.d6920e1d9405925de1b68384f6697dae.css
│ ├── img/
│ │ └── favicons/
│ │ ├── browserconfig.xml
│ │ └── manifest.json
│ ├── js/
│ │ ├── autotrack.js
│ │ ├── browsermodal.js
│ │ └── vendor.dll.js
│ └── manifest.json
└── test/
├── e2e/
│ ├── custom-assertions/
│ │ └── elementCount.js
│ ├── nightwatch.conf.js
│ ├── runner.js
│ └── specs/
│ └── test.js
├── index.js
└── unit/
├── .eslintrc
├── index.js
├── karma.conf.js
└── specs/
└── Hello.spec.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": [
["es2015", { "modules": false }],
"stage-2"
],
"plugins": [
"transform-runtime"
],
"env": {
"test": {
"plugins": [ "istanbul" ]
}
}
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .eslintignore
================================================
src/assets/js/*.js
build/*.js
config/*.js
src/components/UploadAvatar.vue
================================================
FILE: .eslintrc.js
================================================
// http://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}
}
================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
dist/
backups/
npm-debug.log
yarn-error.log
test/unit/coverage
test/e2e/reports
selenium-debug.log
.happypack/
.cache/
================================================
FILE: .npmrc
================================================
registry="https://registry.npm.taobao.org"
================================================
FILE: .prettierignore
================================================
node_modules
dist
backups
src/assets/element
src/assets/scss/bootstrap
.cache
================================================
FILE: CHANGELOG.md
================================================
<a name="1.0.0"></a>
# 1.0.0 (2017-04-19)
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 JadeYang(杨琼璞)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<h1 align="center"><a href="https://site.lovejade.cn/?utm_source=github-nicelinks"><img src="https://image.lovejade.cn/nice-links-logo.png" alt="倾城之链 | NICE LINKS"></a></h1>
<div align="center">
<strong>
🐬 <a href="https://site.lovejade.cn/?utm_source=github-nicelinks">倾城之链</a>,旨在云集全球优秀网站,方便你我探索互联网中更广阔的世界。
</strong>
</div>
<div align="center">
🐬 使用 Vue2 (vuex、vue-router、Webpack4, axios、ES6, Element-Ui...) 构建的 Web 应用程序客户端。
</div>
<br>
<div align="center">
<a href="https://nodejs.org/en/">
<img src="https://img.shields.io/badge/node-%3E%3D%208.0.0-green.svg" alt="node version">
</a>
<a href="https://github.com/nicejade/nicelinks-vue-client">
<img src="https://img.shields.io/github/license/nicejade/nicelinks-vue-client.svg" alt="LICENSE">
</a>
<a href="https://www.jeffjade.com/2017/12/31/136-talk-about-nicelinks-site/">
<img src="https://img.shields.io/badge/chat-on%20blog-brightgreen.svg" alt="Chat On My Blog">
</a>
<a href="https://about.me/nicejade"><img src="https://img.shields.io/badge/Author-nicejade-%23a696c8.svg" alt="Author nicejade"></a>
</div>
## 目标与哲学
您知道,如今这时代,无法估量的信息,广阔无垠且散乱无际;担心错过她而努力汲取的同时,却可能错过更多。当上进心充盈之时,难免会因此感到焦虑与惶恐;故此,有意打造一款优良开放型平台,用来承载信息之入口,并对其进行分门别类、过滤排序,从而为获取、发布信息的双方提供便利 —— 此即为[**「倾城之链」**](https://site.lovejade.cn/?utm_source=github-nicelinks),其旨在云集全球优秀网站,让您更为便捷地探索互联网中那更广阔的世界;在这里,您可以轻松发现、学习、分享更多`有用`或`有趣`的事物。更多关于此项目的动机,详见博客文章:[云集优站,尽在「倾城之链」](https://www.jeffjade.com/2017/12/31/136-talk-about-nicelinks-site/)。
## 作用与价值
### 探索更广阔的世界,为您
> 在这个信息化的世界,海量的讯息可能让您不知所措;担心错过她而努力汲取的同时,却可能错过更多;[「倾城之链」](https://site.lovejade.cn/?utm_source=github-nicelinks)的存在,即是为您解决这种困扰;在这里,您可以浏览全球各类智慧的结晶;丰富视野的同时,可以标注抑或分享您喜欢的站点,从而为更多挖掘讯息的人提供建设性参考。
### 分享,为您所欢喜的网站
> <mark>在当今这信息化时代,即便是再小的个体,也当有自己的品牌。</mark>然而,独立的才是自己的。[「倾城之链」](https://site.lovejade.cn/?utm_source=github-nicelinks)作为一个开放平台,鼓励您创造属于您的个人品牌,烙印着自己的风格,替自己代言、发声;如果您已经这样做了,您可以尽情分享在这里,让更多人知道,且从中受益。当然,您也可以分享出您欢喜的其他有意思站点,让您的见识惠及更多人。
### 箴言锦语 佳句共赏
> 情不知所起,一往而深。那些与旁人说来脸红的绰号暱称、轻灵的诗意,和深刻的激动,像筛子一般,将文字抖出松弛微醺的质感,历久弥新。任时世流转,风华变迁,在这美妙的质感前,循迹而去,仍能感观:那些在文字中留鲜的岁月,一段段永不衰老的情缘;隔著时空漫漫,跨越千山万水,蹁跹而来,带给我们不曾褪色的悸动。**箴言锦语,云集世间曼妙句子**;或情感、或唯美、或修身、或励志、或哲学、或娱乐,拳拳真情,精心择选,总有荡漾你心的那一言。
## 如何访问[「倾城之链」](https://site.lovejade.cn/?utm_source=github.com)
- **WEB 浏览器**:为便捷用户访问,有为[「倾城之链」](https://site.lovejade.cn/?utm_source=github.com)注入 [Pwa](https://github.com/nicejade/nice-front-end-tutorial/blob/master/tutorial/pwa-tutorial.md) 部分功能,您可通过现代浏览器访问 [https://site.lovejade.cn/](https://site.lovejade.cn/?utm_source=github.com) (推荐使用 `Chrome`),可将其“**添加至主屏幕**”,它将为您创建与原生应用类似的桌面图标,以供您下次可便捷且快速打开。
> **备注**:当您在移动设备浏览器点击“**添加到主屏幕**”后,如未得到预期结果,需要您主动进行设置;可在设备`设置`项,找到`权限管理`,进而开启“**桌面快捷方式**”权限即可;这在不同供应商的手机设备间,步骤会有所差别。
- **桌面应用**:如果您想在 `MacOS`, `Windows` 或 `Linux` 中,获得更快捷的访问体验,您可以通过 [Nativefier](https://github.com/jiahaog/nativefier)(使任何网页成为桌面应用程序的一个命令行工具),轻松创建桌面版[「倾城之链」](https://site.lovejade.cn/?utm_source=github.com)应用,只需运行以下命令即可:
```bash
npm install nativefier -g
nativefier --name "倾城之链" "https://site.lovejade.cn//"
```
**备注**:如果您使用最新版本的 Chrome,用其访问 [https://site.lovejade.cn/](https://site.lovejade.cn/?utm_source=github.com) ,在地址栏的末尾,点击 `⊕` 符号,即可快速生成桌面版本「倾城之链」;而且,这这个独立应用中,浏览器插件扩展都在;除此外,浏览器上涉及的[倾城之链](https://site.lovejade.cn/?utm_source=github.com)的外链,也可以右键选择在这个桌面应用中打开,NICE。
## 加入[倾城之链](https://site.lovejade.cn//?utm_source=github.com)群聊
经过近两年的设计,[倾城之链](https://site.lovejade.cn//?utm_source=github.com)目前已趋于稳定;后面将持续迭代,使其拥有[更丰富的功能](https://github.com/nicejade/nicelinks-vue-client/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Afunction)、以及更棒的体验。现创建[倾城之链](https://site.lovejade.cn//?utm_source=github.com)微信、QQ 群(`984154031`),将会不定期分享最新收录的优质网站;如果您感兴趣,不妨考虑加入;另外,您对`倾城之链`有任何意见或建议,也可以通过该群向我反馈。加群具体方式,请参见[加入倾城之链群聊](https://nice.lovejade.cn/zh/application/#加入倾城之链群聊)。
## 相关链接
- [**倾城之链**](https://site.lovejade.cn/?utm_source=github-nicelinks)
- [逍遥自在轩](https://niceshare.site/)
- [晚晴幽草轩](https://www.jeffjade.com/nicelinks?utm_source=github-nicelinks)
- [半缘修道观](https://memo.lovejade.cn/?utm_source=github-nicelinks)
- [玉桃文飨轩](https://share.lovejade.cn/?utm_source=github-nicelinks)
- [静轩之别苑](https://quickapp.lovejade.cn/?utm_source=github-nicelinks)
- [SegmentFault](https://segmentfault.com/u/jeffjade)
- [X | MarshalXuan](https://x.com/MarshalXuan)
- [Facebook](https://www.facebook.com/nice.jade.yang)
## 许可执照
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2017-present, [nicejade](https://site.lovejade.cn//member/admin)
================================================
FILE: ads.txt
================================================
google.com, pub-8586652723015758, DIRECT, f08c47fec0942fa0
================================================
FILE: build/build.js
================================================
// https://github.com/shelljs/shelljs
require('./check-versions')()
require('shelljs/global')
env.NODE_ENV = process.env.NODE_ENV || 'production'
var ora = require('ora')
var path = require('path')
var chalk = require('chalk')
var shell = require('shelljs')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
// backups for build @2017-11-12
var backupsSpinner = ora('Start backing up the last packaged project...')
backupsSpinner.start()
let backupsPath = path.resolve(__dirname, '../backups')
shell.rm('-rf', backupsPath)
shell.mkdir('-p', backupsPath)
shell.cp('-R', config.build.assetsRoot + '/*', backupsPath)
backupsSpinner.stop()
var spinner = ora('building for production...')
spinner.start()
var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory)
shell.rm('-rf', assetsPath)
shell.mkdir('-p', assetsPath)
shell.config.silent = true
shell.cp('-R', 'static/*', assetsPath)
shell.cp('./ads.txt', config.build.assetsRoot)
shell.config.silent = false
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(
stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
}) + '\n\n'
)
console.log(chalk.cyan(' Build complete.\n'))
console.log(
chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
" Opening index.html over file:// won't work.\n"
)
)
})
================================================
FILE: build/check-versions.js
================================================
var chalk = require('chalk')
var semver = require('semver')
var packageConfig = require('../package.json')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
var versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
{
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
}
]
module.exports = function () {
var warnings = []
for (var i = 0; i < versionRequirements.length; i++) {
var mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (var i = 0; i < warnings.length; i++) {
var warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
================================================
FILE: build/deploy.js
================================================
var path = require('path')
var shell = require('shelljs')
var chalk = require('chalk')
let sourcePath = path.resolve(__dirname, '../dist/*')
// Copy To nicelinks.site
let targetPath = './../nicelinks.site/server/'
shell.rm('-rf', `${targetPath}public/static`)
shell.mkdir('-p', `${targetPath}public/static`)
shell.cp('-R', sourcePath, `${targetPath}public/`)
// shell.cd(targetPath)
// if (!shell.which('git')) {
// shell.echo('Sorry, this script requires git')
// shell.exit(1)
// }
// if (shell.exec('git commit -am "Auto-commit"').code !== 0) {
// shell.echo('Error: Git commit failed')
// shell.exit(1)
// }
// shell.config.silent = true
// shell.config.silent = false
console.log(chalk.green('\n√ Success: ') + 'Deploy Has been completed.')
================================================
FILE: build/dev-client.js
================================================
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(function (event) {
if (event.action === 'reload') {
window.location.reload()
}
})
================================================
FILE: build/dev-server.js
================================================
require('./check-versions')()
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf')
// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
devMiddleware.waitUntilValid(function () {
console.log('> Listening at ' + uri + '\n')
})
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
return
}
// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
})
================================================
FILE: build/generate-sitemap.js
================================================
const sitemapGenerator = require('sitemap-generator');
const path = require('path')
// create generator
const generator = sitemapGenerator('https://nicelinks.site/?_escaped_fragment_', {
maxDepth: 4,
filepath: path.join(process.cwd(), '/dist/sitemap.xml'),
maxEntriesPerFile: 50000,
stripQuerystring: false
});
generator.on('error', (error) => {
console.log(`🐛 Opps, Something error:`);
console.log(error)
});
// register event listeners
generator.on('done', () => {
console.log('😊 okay, sitemaps have created.')
});
// start the crawler
generator.start();
================================================
FILE: build/load-minified.js
================================================
'use strict'
const fs = require('fs')
const UglifyJS = require('uglify-es')
module.exports = function(filePath) {
const code = fs.readFileSync(filePath, 'utf-8')
const result = UglifyJS.minify(code)
if (result.error) return ''
return result.code
}
================================================
FILE: build/service-worker-dev.js
================================================
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// In the production build, this file is replaced with an actual service worker
// file that will precache your site's local assets.
// See https://github.com/facebookincubator/create-react-app/issues/2272#issuecomment-302832432
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => {
self.clients.matchAll({ type: 'window' }).then(windowClients => {
for (let windowClient of windowClients) {
// Force open pages to refresh, so that they have a chance to load the
// fresh navigation response from the local dev server.
windowClient.navigate(windowClient.url);
}
});
});
================================================
FILE: build/service-worker-prod.js
================================================
(function() {
'use strict';
// Check to make sure service workers are supported in the current browser,
// and that the current page is accessed from a secure origin. Using a
// service worker from an insecure origin will trigger JS console errors.
const isLocalhost = Boolean(window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
window.addEventListener('load', function() {
if ('serviceWorker' in navigator &&
(window.location.protocol === 'https:' || isLocalhost)) {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
// updatefound is fired if service-worker.js changes.
registration.onupdatefound = function() {
// updatefound is also fired the very first time the SW is installed,
// and there's no need to prompt for a reload at that point.
// So check here to see if the page is already controlled,
// i.e. whether there's an existing service worker.
if (navigator.serviceWorker.controller) {
// The updatefound event implies that registration.installing is set
const installingWorker = registration.installing;
installingWorker.onstatechange = function() {
switch (installingWorker.state) {
case 'installed':
// At this point, the old content will have been purged and the
// fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in the page's interface.
break;
case 'redundant':
throw new Error('The installing ' +
'service worker became redundant.');
default:
// Ignore
}
};
}
};
}).catch(function(e) {
console.error('Error during service worker registration:', e);
});
}
});
})();
================================================
FILE: build/utils.js
================================================
const path = require('path')
const config = require('../config')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
if (options.extract) {
return [MiniCssExtractPlugin.loader].concat(loaders)
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
================================================
FILE: build/vendor-manifest.json
================================================
{"name":"vendor_library","content":{"./node_modules/axios/lib/utils.js":{"id":0,"buildMeta":{"providedExports":true}},"./node_modules/webpack/buildin/global.js":{"id":1,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/defaults.js":{"id":2,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/src/utils.js":{"id":3,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/bind.js":{"id":4,"buildMeta":{"providedExports":true}},"./node_modules/process/browser.js":{"id":5,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/adapters/xhr.js":{"id":6,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/createError.js":{"id":7,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/isCancel.js":{"id":8,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/Cancel.js":{"id":9,"buildMeta":{"providedExports":true}},"./node_modules/crypto-js/core.js":{"id":10,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/vendor/json-stringify-safe/stringify.js":{"id":11,"buildMeta":{"providedExports":true}},"./node_modules/js-cookie/src/js.cookie.js":{"id":13,"buildMeta":{"providedExports":true}},"./node_modules/axios/index.js":{"id":14,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/axios.js":{"id":15,"buildMeta":{"providedExports":true}},"./node_modules/axios/node_modules/is-buffer/index.js":{"id":16,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/Axios.js":{"id":17,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/normalizeHeaderName.js":{"id":18,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/settle.js":{"id":19,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/enhanceError.js":{"id":20,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/buildURL.js":{"id":21,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/parseHeaders.js":{"id":22,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/isURLSameOrigin.js":{"id":23,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/cookies.js":{"id":24,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/InterceptorManager.js":{"id":25,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/dispatchRequest.js":{"id":26,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/core/transformData.js":{"id":27,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/isAbsoluteURL.js":{"id":28,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/combineURLs.js":{"id":29,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/cancel/CancelToken.js":{"id":30,"buildMeta":{"providedExports":true}},"./node_modules/axios/lib/helpers/spread.js":{"id":31,"buildMeta":{"providedExports":true}},"./node_modules/vue/dist/vue.min.js":{"id":32,"buildMeta":{"providedExports":true}},"./node_modules/timers-browserify/main.js":{"id":33,"buildMeta":{"providedExports":true}},"./node_modules/setimmediate/setImmediate.js":{"id":34,"buildMeta":{"providedExports":true}},"./node_modules/vue-router/dist/vue-router.esm.js":{"id":35,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}},"./node_modules/vuex/dist/vuex.esm.js":{"id":36,"buildMeta":{"exportsType":"namespace","providedExports":["Store","install","mapState","mapMutations","mapGetters","mapActions","createNamespacedHelpers","default"]}},"./node_modules/crypto-js/sha256.js":{"id":37,"buildMeta":{"providedExports":true}},"./node_modules/crypto-js/md5.js":{"id":38,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/src/singleton.js":{"id":39,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/src/raven.js":{"id":40,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/vendor/TraceKit/tracekit.js":{"id":41,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/vendor/md5/md5.js":{"id":42,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/src/configError.js":{"id":43,"buildMeta":{"providedExports":true}},"./node_modules/raven-js/src/console.js":{"id":44,"buildMeta":{"providedExports":true}},"./node_modules/vue-content-placeholder/dist/vue-content-placeholder.min.js":{"id":45,"buildMeta":{"moduleConcatenationBailout":"eval()","providedExports":true}},"./node_modules/mark.js/dist/mark.js":{"id":46,"buildMeta":{"providedExports":true}},"./node_modules/marked/lib/marked.umd.js":{"id":47,"buildMeta":{"providedExports":true}},"./node_modules/medium-zoom/dist/medium-zoom.esm.js":{"id":48,"buildMeta":{"exportsType":"namespace","providedExports":["default"]}}}}
================================================
FILE: build/vue-loader.conf.js
================================================
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'
module.exports = {
loaders: utils.cssLoaders({
sourceMap: isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap,
extract: isProduction
}),
postcss: [
require('autoprefixer')({
browsers: ['last 2 versions']
})
]
}
================================================
FILE: build/webpack.base.conf.js
================================================
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const webpack = require('webpack')
const autoprefixer = require('autoprefixer')
const vueLoaderConfig = require('./vue-loader.conf')
const svgoConfig = require('../config/svgo-config.json')
const chalk = require('chalk')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const env = process.env.NODE_ENV
const resolve = (dir) => {
return path.join(__dirname, '..', dir)
}
const cssLoader = new MiniCssExtractPlugin({
use: ['happypack/loader?id=happy-css'],
})
// inject happypack accelerate packing for vue-loader @17-08-18
Object.assign(vueLoaderConfig.loaders, {
js: 'happypack/loader?id=happy-babel-vue',
css: cssLoader,
})
function createHappyPlugin(id, loaders) {
return new HappyPack({
id: id,
loaders: loaders,
threadPool: happyThreadPool,
// make happy more verbose with HAPPY_VERBOSE=1
verbose: process.env.HAPPY_VERBOSE === '1',
})
}
module.exports = {
entry: {
app: './src/main.js',
element: ['element-ui'],
},
mode: env === 'production' ? 'production' : 'development',
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath:
process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath,
},
resolve: {
extensions: ['.js', '.vue', '.json'],
modules: [resolve('src'), resolve('node_modules')],
alias: {
vue$: 'vue/dist/vue.min.js',
src: resolve('src'),
assets: resolve('src/assets'),
components: resolve('src/components'),
config: resolve('src/config'),
helper: resolve('src/helper'),
views: resolve('src/views'),
mixins: resolve('src/mixins'),
partials: resolve('src/partials'),
store: resolve('src/store'),
},
},
module: {
noParse: /node_modules\/(element-ui\.js)/,
rules: [
{
test: /\.svg$/,
enforce: 'pre',
loader: 'svgo-loader?' + JSON.stringify(svgoConfig),
include: /assets\/icons/,
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig,
include: [resolve('src')],
exclude: /node_modules\/(?!(autotrack))|vendor\.dll\.js/,
},
{
test: /\.js[x]?$/,
exclude: /node_modules/,
include: [resolve('src')],
loader: 'happypack/loader?id=happy-babel-js',
},
// inject loader for @waline/dist
{
test: /\.js[x]?$/,
include: [/node_modules\/@waline/],
loader: 'babel-loader',
},
{
test: /\.svg$/,
loader: 'happypack/loader?id=happy-svg',
include: [/assets\/icons/, /node_modules\/mavon-editor/],
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
include: [resolve('src')],
exclude: /assets\/icons/,
query: {
limit: 8192,
name: utils.assetsPath('img/[name].[hash:7].[ext]'),
},
},
{
test: /\.(woff2?|eot|woff|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
query: {
limit: 8192,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]'),
},
},
],
},
// externals中:key 是 require 的包名,value 是全局的变量。
externals: {
// 'element-ui': 'ElementUI',
// 'vue': 'Vue',
'babel-polyfill': 'window',
},
plugins: [
new ProgressBarPlugin({
format: ' build [:bar] ' + chalk.green.bold(':percent') + ' (:elapsed seconds)',
}),
new webpack.DllReferencePlugin({
context: path.resolve(__dirname, '..'),
manifest: require('./vendor-manifest.json'),
}),
new VueLoaderPlugin(),
new webpack.LoaderOptionsPlugin({
options: {
postcss: [
autoprefixer({
browsers: ['last 2 version'],
}),
],
},
}),
createHappyPlugin('happy-babel-js', ['babel-loader?cacheDirectory=true']),
createHappyPlugin('happy-babel-vue', ['babel-loader?cacheDirectory=true']),
createHappyPlugin('happy-svg', ['svg-sprite-loader']),
createHappyPlugin('happy-css', ['css-loader']),
new HappyPack({
loaders: [
{
path: 'vue-loader',
query: {
loaders: {
scss: 'css-loader!sass-loader?indentedSyntax',
js: 'happypack/loader?id=happy-babel-vue',
},
},
},
],
}),
],
}
================================================
FILE: build/webpack.dev.conf.js
================================================
const fs = require('fs')
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// cheap-module-eval-source-map is faster for development
// devtool: '#cheap-module-eval-source-map',
devtool: '#eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.ejs',
inject: true,
serviceWorkerLoader: `<script>${fs.readFileSync(path.join(__dirname,
'./service-worker-dev.js'), 'utf-8')}</script>`
}),
new FriendlyErrorsPlugin()
]
})
================================================
FILE: build/webpack.dll.conf.js
================================================
const path = require('path')
const webpack = require('webpack')
const TerserPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
entry: {
vendor: [
'js-cookie',
'axios',
'vue/dist/vue.min.js',
'vue-router',
'vuex',
'crypto-js/sha256',
'crypto-js/md5',
'raven-js',
'vue-content-placeholder',
'mark.js',
'marked',
'medium-zoom'
],
},
output: {
path: path.resolve(__dirname, '../static/js'),
filename: '[name].dll.js',
library: '[name]_library',
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules\/(?!(autotrack|dom-utils))/,
},
],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
parallel: true,
extractComments: false,
terserOptions: {
ecma: undefined,
parse: {},
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
mangle: true,
module: true,
output: null,
format: { comments: false },
toplevel: false,
nameCache: null,
ie8: false,
keep_classnames: undefined,
keep_fnames: false,
safari10: false,
},
}),
new OptimizeCSSAssetsPlugin({}),
],
},
plugins: [
/*
@desc: https://webpack.js.org/plugins/module-concatenation-plugin/
"作用域提升(scope hoisting)",使代码体积更小[函数申明会产生大量代码](#webpack3)
*/
new webpack.optimize.ModuleConcatenationPlugin(),
new webpack.DllPlugin({
path: path.join(__dirname, '.', '[name]-manifest.json'),
name: '[name]_library',
}),
],
}
================================================
FILE: build/webpack.prod.conf.js
================================================
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
// const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin')
const loadMinified = require('./load-minified')
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const BabiliWebpackPlugin = require('babili-webpack-plugin')
const Jarvis = require('webpack-jarvis')
const env = process.env.NODE_ENV === 'testing' ? require('../config/test.env') : config.build.env
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
}),
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
// @desc: Documenttion: https://webpack.js.org/configuration/performance/
performance: {
// Given an asset is created that is over 250kb;false | "error" | "warning"(Default)
hints: 'warning',
// The default value is 250000 (bytes).
maxEntrypointSize: 500000, // (300kb)
// This option controls when webpack emits a performance hint based on individual asset size. The default value is 250000 (bytes).
maxAssetSize: 500000,
},
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js'),
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
test: /\.js(\?.*)?$/i,
parallel: true,
extractComments: false,
terserOptions: {
ecma: undefined,
parse: {},
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
mangle: true,
module: true,
output: null,
format: { comments: false },
toplevel: false,
nameCache: null,
ie8: false,
keep_classnames: undefined,
keep_fnames: false,
safari10: false,
},
}),
],
/*
@desc: Setting optimization.runtimeChunk to true adds an additonal chunk to each entrypoint containing only the runtime.
The value single instead creates a runtime file to be shared for all generated chunks.
@reference: https://doc.webpack-china.org/plugins/split-chunks-plugin/#optimization-runtimechunk
*/
// runtimeChunk: {
// name: 'manifest'
// }
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env,
}),
// extract css into its own file
new MiniCssExtractPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSAssetsPlugin({}),
// A Webpack Plugin for Babili - A babel based minifier
// https://www.npmjs.com/package/babili-webpack-plugin
new BabiliWebpackPlugin(),
// @desc: Documentation:https://www.webpackjs.com/plugins/split-chunks-plugin/
new webpack.optimize.SplitChunksPlugin({
// chunks: "initial","async"和"all"分别是:初始块,按需块或所有块;
chunks: 'all',
// (默认值:30000)块的最小大小
minSize: 50000,
// (默认值:1)分割前共享模块的最小块数
minChunks: 1,
// (缺省值5)按需加载时的最大并行请求数
maxAsyncRequests: 6,
// (默认值3)入口点上的最大并行请求数
maxInitialRequests: 6,
// webpack 将使用块的起源和名称来生成名称: `vendors~main.js`,如项目与"~"冲突,则可通过此值修改,Eg: '-'
automaticNameDelimiter: '~',
// cacheGroups is an object where keys are the cache group names.
name: true,
cacheGroups: {
// 设置为 false 以禁用默认缓存组
default: true,
element: {
name: 'element',
test: /[\\/]node_modules[\\/]element-ui[\\/]/,
chunks: 'initial',
// 默认组的优先级为负数,以允许任何自定义缓存组具有更高的优先级(默认值为0)
priority: 1000,
},
// lodash: {
// name: 'lodash',
// test: /[\\/]node_modules[\\/]lodash[\\/]/,
// chunks: 'initial',
// // 默认组的优先级为负数,以允许任何自定义缓存组具有更高的优先级(默认值为0)
// priority: 1000,
// },
},
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: process.env.NODE_ENV === 'testing' ? 'index.html' : config.build.index,
template: 'index.ejs',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
// @reference: https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via splitChunks
chunksSortMode: 'dependency',
serviceWorkerLoader: `<script type="text/javascript">${loadMinified(
path.join(__dirname, './service-worker-prod.js')
)}</script>`,
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, 'dist/*.dll.js'),
}),
// copy custom static assets (已在 build.js 中通过 Shell 做了,所以可不用此插件;)
// new CopyWebpackPlugin([
// {
// from: path.resolve(__dirname, '../static'),
// to: config.build.assetsSubDirectory,
// ignore: ['.*']
// }
// ]),
/*
@desc: service worker caching, More detailed configuration:
@reference: https://github.com/goldhand/sw-precache-webpack-plugin
*/
new SWPrecacheWebpackPlugin({
cacheId: 'nicelinks',
filename: 'service-worker.js',
staticFileGlobs: ['dist/**/*.{js,html,css}'],
minify: true,
stripPrefix: 'dist/',
}),
new LodashModuleReplacementPlugin(),
/*
@desc: limit minChunkSize through MinChunkSizePlugin
@reference: https://webpack.js.org/plugins/min-chunk-size-plugin/
*/
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 50000, // Minimum number of characters (25kb)
}),
/*
@desc: AggressiveSplittingPlugin 可以将 bundle 拆分成更小的 chunk;
直到各个 chunk 的大小达到 option 设置的 maxSize,它通过目录结构将模块组织在一起
@reference: https://doc.webpack-china.org/plugins/aggressive-splitting-plugin/
@but: 由于 HtmlWebpackPlugin 插件中的错误,此方法在启用时不起作用;
具体可参见:https://github.com/jantimon/html-webpack-plugin/issues/446
*/
// new webpack.optimize.AggressiveSplittingPlugin({
// minSize: 10000,
// maxSize: 500000
// }),
/*
@desc: 编译之后,您可能会注意到某些块太小 - 创建更大的HTTP开销,那么您可以处理像这样;
@reference: https://webpack.js.org/plugins/limit-chunk-count-plugin/
*/
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 10, // Must be greater than or equal to one
// minChunkSize: 1000
}),
// 在编译出现错误时,使用 NoEmitOnErrorsPlugin 来跳过输出阶段;
new webpack.NoEmitOnErrorsPlugin(),
/*
@desc: 提升代码在浏览器中的执行速度: 作用域提升(scope hoisting);
@reference: https://doc.webpack-china.org/plugins/module-concatenation-plugin/
*/
new webpack.optimize.ModuleConcatenationPlugin(),
],
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp('\\.(' + config.build.productionGzipExtensions.join('|') + ')$'),
threshold: 10240,
minRatio: 0.8,
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
if (config.build.bundleIntelligentDashboard) {
webpackConfig.plugins.push(
new Jarvis({
port: 1337,
})
)
}
module.exports = webpackConfig
================================================
FILE: build/webpack.test.conf.js
================================================
// This is the webpack config used for unit tests.
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
var baseConfig = require('./webpack.base.conf')
var webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
rules: utils.styleLoaders()
},
devtool: '#inline-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/test.env')
})
]
})
// no need for app entry during tests
delete webpackConfig.entry
module.exports = webpackConfig
================================================
FILE: config/dev.env.js
================================================
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})
================================================
FILE: config/index.js
================================================
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')
let publicPathPrefix = process.env.NODE_ENV === 'production' ? 'https://static.nicelinks.site/' : '/'
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: publicPathPrefix,
productionSourceMap: false,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
port: 8888,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': 'http://localhost:4000'
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
}
================================================
FILE: config/prod.env.js
================================================
module.exports = {
NODE_ENV: '"production"'
}
================================================
FILE: config/svgo-config.json
================================================
{
"plugins": [
{
"cleanupAttrs": true
},
{
"cleanupEnableBackground": true
},
{
"cleanupIDs": true
},
{
"cleanupListOfValues": true
},
{
"cleanupNumericValues": true
},
{
"collapseGroups": true
},
{
"convertColors": true
},
{
"convertPathData": true
},
{
"convertShapeToPath": true
},
{
"convertStyleToAttrs": true
},
{
"convertTransform": true
},
{
"mergePaths": true
},
{
"moveElemsAttrsToGroup": true
},
{
"moveGroupAttrsToElems": true
},
{
"removeComments": true
},
{
"removeDesc": true
},
{
"removeDimensions": true
},
{
"removeDoctype": true
},
{
"removeEditorsNSData": true
},
{
"removeEmptyAttrs": true
},
{
"removeEmptyContainers": true
},
{
"removeEmptyText": true
},
{
"removeHiddenElems": true
},
{
"removeMetadata": true
},
{
"removeNonInheritableGroupAttrs": true
},
{
"removeRasterImages": true
},
{
"removeTitle": true
},
{
"removeUnknownsAndDefaults": true
},
{
"removeUselessDefs": true
},
{
"removeUnusedNS": true
},
{
"removeUselessStrokeAndFill": true
},
{
"removeXMLProcInst": true
},
{
"sortAttrs": true
}
]
}
================================================
FILE: config/test.env.js
================================================
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})
================================================
FILE: index.ejs
================================================
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>倾城之链 | NICE LINKS</title>
<meta name="description" content="倾城之链,作为一个开放平台,旨在云集全球优秀网站,探索互联网中更广阔的世界;在这里,你可以轻松发现、学习、分享更多有用或有趣的事物。" />
<meta name="keywords" content="网站, 应用, 导航, 分享, 有趣, 学习, 网址导航, 网址大全, 免费资源, 最新网址, 上网导航, 国外网站, 网站优化, 资源分享, 视频下载, 实用软件, 科技网站, 学习强国, 分享赚钱, 睡后收入, 被动收入" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="home" href="https://nicelinks.site/" />
<meta name="viewport" content="user-scalable=yes,width=device-width,initial-scale=1,maximum-scale=3">
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@nicejadeyang" />
<meta name="twitter:title" content="倾城之链 | NICE LINKS" />
<meta name="twitter:description" content="倾城之链,作为一个开放平台,旨在云集全球优秀网站,探索互联网中更广阔的世界;在这里,你可以轻松发现、学习、分享更多有用或有趣的事物。" />
<meta name="twitter:image" content="https://static.nicelinks.site/static/img/favicons/favicon.png" />
<link rel="preconnect" href="https://static.nicelinks.site" crossorigin>
<link rel="dns-prefetch" href="https://static.nicelinks.site">
<link rel="preconnect" href="https://image.nicelinks.site" crossorigin>
<link rel="dns-prefetch" href="https://image.nicelinks.site">
<link rel="preconnect" href="https://oss.nicelinks.site" crossorigin>
<link rel="dns-prefetch" href="https://oss.nicelinks.site">
<link rel="dns-prefetch" href="https://comment.nicelinks.site">
<link rel="dns-prefetch" href="https://pagead2.googlesyndication.com">
<link rel="dns-prefetch" href="https://www.googletagmanager.com">
<script data-ad-client="ca-pub-8586652723015758" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<% if (htmlWebpackPlugin.options.env !== 'development') { %>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-L1T4772VPY"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-L1T4772VPY');
gtag('config', 'AW-11385088411');
if (location.href.indexOf('google_ads') > -1) {
window.IS_FROM_GOOGLE_ADS = true
}
function gtag_report_conversion(url) {
var callback = function () {
if (typeof(url) != 'undefined') {
window.location = url;
}
};
gtag('event', 'conversion', {
'send_to': 'AW-11385088411/8Y-PCKrJn-8YEJvT6rQq',
'event_callback': callback
});
return false;
}
</script>
<!-- Global site tag (gtag.js) - Google Analytics End-->
<link rel="apple-touch-icon" sizes="180x180" href="/static/img/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/img/favicons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/img/favicons/favicon-16x16.png">
<!--[if IE]><link rel="shortcut icon" href="/static/img/favicons/favicon.ico"><![endif]-->
<link rel="manifest" href="/static/manifest.json">
<link rel="mask-icon" href="/static/img/favicons/safari-pinned-tab.svg" color="#27a0cf">
<meta name="theme-color" content="#ffffff">
<% for (var chunk of webpack.chunks) {
for (var file of chunk.files) {
if (file.match(/\.(css)$/)) { %>
<link rel="<%= chunk.initial ? 'preload' : 'prefetch' %>" href="<%= htmlWebpackPlugin.files.publicPath + file %>" as="<%= file.match(/\.css$/)?'style':'script' %>">
<% }}} %>
<% } %>
<script type="text/javascript">
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "ksgtwkg7t0");
</script>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="<%= webpackConfig.output.publicPath %>static/js/vendor.dll.js?v=20230311"></script>
<!--[if lt IE 9]>
<script async src="<%= webpackConfig.output.publicPath %>static/js/browsermodal.js"></script>
<![endif]-->
<!-- Todo: only include in production -->
<%= htmlWebpackPlugin.options.serviceWorkerLoader %>
<!-- built files will be auto injected -->
</script>
</body>
</html>
================================================
FILE: package.json
================================================
{
"name": "nicelinks-vue-client",
"version": "3.11.3",
"description": "A nice website for assembling nice links created using Vue.js",
"author": "jeffjade <jeffygisgreat@gmail.com>",
"private": true,
"scripts": {
"dev": "node build/dev-server.js",
"start": "node build/dev-server.js",
"commit": "git add . && git cz && git pull && git push",
"deploy": "npm run build && node build/deploy.js",
"build": "NODE_ENV=production node build/build.js --progress --no-warnings",
"build:dll": "NODE_ENV=production webpack --config build/webpack.dll.conf.js",
"prettier": "prettier --write \"src/**/*.{js,css,scss,vue}\"",
"unit": "cross-env NODE_ENV=production cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
"pretest": "cross-env NODE_ENV=testing node build/build.js && node test/index.js",
"e2e": "node test/e2e/runner.js",
"test-e2e": "npm run unit && npm run e2e",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-staged": "lint-staged",
"precommit-msg": "echo 'Pre-commit checks...' && exit 0",
"analyz": "NODE_ENV=production npm_config_report=true npm run build",
"clean-commit": "git checkout --orphan latest_branch && git add -A && git commit -am 'style: clean past commit history 😊' && git branch -D master && git branch -m master && git push -f origin master",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
"generate-sitemap": "node build/generate-sitemap.js"
},
"pre-commit": [
"precommit-msg",
"lint-staged"
],
"lint-staged": {
"src/**.{js,json,pcss,md,vue,css,scss}": [
"prettier --write",
"git add"
]
},
"prettier": {
"singleQuote": true,
"semi": false,
"printWidth": 100,
"proseWrap": "never"
},
"dependencies": {
"@waline/client": "2.10.0",
"axios": "^0.18.1",
"countup": "^1.8.2",
"crypto-js": "^3.1.9-1",
"element-ui": "2.15.13",
"hint.css": "^2.6.0",
"js-cookie": "^2.1.3",
"lodash": "^4.17.21",
"mark.js": "^8.11.1",
"marked": "^4.1.0",
"medium-zoom": "^1.0.8",
"qrcode.vue": "1.7.0",
"raven-js": "^3.19.1",
"vue": "2.7.10",
"vue-content-placeholder": "^1.1.1",
"vue-router": "^2.2.0",
"vuex": "^2.3.1"
},
"devDependencies": {
"add-asset-html-webpack-plugin": "^2.1.3",
"autoprefixer": "^6.7.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.1.1",
"babel-loader": "^6.2.10",
"babel-plugin-component": "^1.1.1",
"babel-plugin-istanbul": "^3.1.2",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-preset-env": "^1.2.1",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.22.0",
"babel-register": "^6.22.0",
"babili-webpack-plugin": "^0.1.2",
"chalk": "^1.1.3",
"connect-history-api-fallback": "^1.3.0",
"copy-webpack-plugin": "^4.0.1",
"cross-env": "^3.1.4",
"css-hot-loader": "^1.3.9",
"css-loader": "^0.26.1",
"eslint": "^4.18.2",
"eslint-config-standard": "^6.2.1",
"eslint-friendly-formatter": "^2.0.7",
"eslint-loader": "^1.6.1",
"eslint-plugin-html": "3.0.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^2.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.14.1",
"file-loader": "^0.10.0",
"friendly-errors-webpack-plugin": "^1.1.3",
"happypack": "^5.0.0-beta.1",
"html-webpack-plugin": "^3.2.0",
"http-proxy-middleware": "^0.17.3",
"lint-staged": "^3.6.1",
"lodash-webpack-plugin": "^0.11.4",
"lodash.clonedeep": "^4.5.0",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^6.0.1",
"opn": "^4.0.2",
"optimize-css-assets-webpack-plugin": "^1.3.0",
"ora": "^1.1.0",
"postcss-loader": "^2.1.5",
"pre-commit": "^1.2.2",
"prettier": "^2.0.5",
"progress-bar-webpack-plugin": "^1.9.3",
"sass-loader": "^4.0.2",
"scss-loader": "^0.0.1",
"shelljs": "^0.7.8",
"standard": "^9.0.1",
"style-loader": "^0.21.0",
"svg-sprite-loader": "^0.3.0",
"svgo": "^0.7.1",
"svgo-loader": "^1.1.0",
"svgxuse": "^1.1.16",
"sw-precache-webpack-plugin": "^0.11.4",
"terser-webpack-plugin": "4.2.3",
"uglify-es": "^3.3.9",
"url-loader": "^0.5.7",
"vue-loader": "15.10.0",
"vue-template-compiler": "2.7.10",
"webpack": "^4.6.0",
"webpack-build-notifier": "^0.1.13",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-cli": "3.3.10",
"webpack-dev-middleware": "^1.10.0",
"webpack-hot-middleware": "^2.16.1",
"webpack-jarvis": "^0.2.0",
"webpack-merge": "^2.6.1"
},
"engines": {
"node": ">= 4.0.0",
"npm": ">= 3.0.0"
},
"keywords": [
"倾城之链",
"NICE LINKS",
"开放平台",
"网址大全",
"网址导航"
],
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
================================================
FILE: src/App.vue
================================================
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
import 'element-ui/lib/theme-chalk/display.css'
import 'element-ui/lib/theme-chalk/pagination.css'
import 'element-ui/lib/theme-chalk/dialog.css'
import 'element-ui/lib/theme-chalk/icon.css'
import 'element-ui/lib/theme-chalk/input.css'
import 'element-ui/lib/theme-chalk/switch.css'
import 'element-ui/lib/theme-chalk/select.css'
import 'element-ui/lib/theme-chalk/option.css'
import 'element-ui/lib/theme-chalk/button.css'
import 'element-ui/lib/theme-chalk/table.css'
import 'element-ui/lib/theme-chalk/table-column.css'
import 'element-ui/lib/theme-chalk/breadcrumb.css'
import 'element-ui/lib/theme-chalk/breadcrumb-item.css'
import 'element-ui/lib/theme-chalk/form.css'
import 'element-ui/lib/theme-chalk/form-item.css'
import 'element-ui/lib/theme-chalk/tabs.css'
import 'element-ui/lib/theme-chalk/tab-pane.css'
import 'element-ui/lib/theme-chalk/loading.css'
import 'element-ui/lib/theme-chalk/tag.css'
import 'element-ui/lib/theme-chalk/alert.css'
import 'element-ui/lib/theme-chalk/card.css'
import 'element-ui/lib/theme-chalk/carousel.css'
import 'element-ui/lib/theme-chalk/message.css'
import 'element-ui/lib/theme-chalk/message-box.css'
import 'element-ui/lib/theme-chalk/carousel-item.css'
import './assets/scss/style.scss'
export default {
name: 'app',
}
</script>
================================================
FILE: src/assets/icons/index.js
================================================
const files = require.context('.', true, /\.svg$/)
const modules = {}
files.keys().forEach((key) => {
modules[key.replace(/(\.\/|\.svg)/g, '')] = files(key)
})
export default modules
================================================
FILE: src/assets/scss/colors.scss
================================================
$emotion: $jade;
$encourage: #0099ff;
$entertainment: #ff6666;
$self-cultivation: #8552a1;
$aestheticism: #2edfa3;
$philosophy: #333366;
.emotion-colors {
color: $emotion;
border-color: $emotion;
}
.encourage-colors {
color: $encourage;
border-color: $encourage;
}
.entertainment-colors {
color: $entertainment;
border-color: $entertainment;
}
.self-cultivation-colors {
color: $self-cultivation;
border-color: $self-cultivation;
}
.aestheticism-colors {
color: $aestheticism;
border-color: $aestheticism;
}
.philosophy-colors {
color: $philosophy;
border-color: $philosophy;
}
#awesome-sentence {
.encourage-colors {
color: #ffffcc;
fill: #ffffcc;
background-color: $encourage;
border: 1px solid $encourage !important;
}
.entertainment-colors {
color: #ffff00;
fill: #ffff00;
background-color: $entertainment;
border: 1px solid #ffff00;
}
.self-cultivation-colors {
color: #fefefe;
fill: #fefefe;
background-color: $self-cultivation;
border: 1px solid #333333;
}
.aestheticism-colors {
color: #ffffff;
fill: #ffffff;
background-color: $aestheticism;
border: 1px solid #ffffff;
}
.philosophy-colors {
color: #fefefe;
fill: #fefefe;
background-color: $philosophy;
border: 1px solid #fefefe;
}
}
================================================
FILE: src/assets/scss/common.scss
================================================
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
*:focus {
outline: none !important;
}
body {
font: normal 14px/1 'Open Sans', 'SF Pro Text', 'Myriad Set Pro', 'SF Pro Icons', 'Helvetica Neue',
'Helvetica', 'Arial', sans-serif;
}
html {
font-size: 62.5%;
height: 100%;
transition: color 300ms, background-color 300ms;
}
body[theme='dark'] {
overflow: auto;
filter: invert(1) hue-rotate(180deg);
}
body[theme='dark'] img,
body[theme='dark'] iframe {
filter: invert(1) hue-rotate(180deg);
}
.filter-grayscale {
-webkit-filter: grayscale(100%);
filter: grayscale(100%);
}
@media (max-width: 960px) {
html {
font-size: 65%;
}
}
h1,
h2,
h3,
h4,
h5 {
font-weight: normal;
margin: 0;
padding: 0;
line-height: 1;
}
h2 {
font-size: $font-large;
}
ol,
ul {
list-style: none;
}
em {
font-style: normal;
}
strong {
font-weight: 500;
}
textarea {
resize: vertical;
}
table {
border-collapse: collapse;
border-spacing: 0;
th {
font-weight: normal;
}
}
label {
font-weight: normal;
}
a {
color: $brand;
text-decoration: none;
}
a:hover,
a:visited,
a:link {
color: $brand;
text-decoration: none;
}
input::-webkit-input-placeholder {
color: $input-placeholder !important;
}
input::-moz-placeholder {
color: $input-placeholder !important;
}
input:-moz-placeholder {
color: $input-placeholder !important;
}
input:-ms-input-placeholder {
color: $input-placeholder !important;
}
.el-input__icon {
color: $input-placeholder !important;
}
.panel-body {
padding: $panel-body-padding;
@include clearfix;
}
.flex-box {
display: -webkit-box;
display: -moz-box;
display: -webkit-flex;
display: flex;
-webkit-box-align: center;
-moz-box-align: center;
-webkit-align-items: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
justify-content: center;
}
.text-center {
text-align: center;
}
.button {
position: relative;
left: 50%;
transform: translateX(-50%);
-webkit-transform: translateX(-50%);
width: 200px;
height: 40px;
line-height: 40px;
border-radius: 20px;
color: #000;
font-size: 20px;
letter-spacing: 1px;
font-weight: 200;
@include flex-box-center;
}
.form-group {
text-align: left;
margin-bottom: 25px;
.col-sm-3 {
display: inline-block;
width: 24.5%;
text-align: left;
}
.col-sm-9 {
display: inline-block;
width: 73.5%;
}
}
.gray {
color: $silver-grey;
}
.form-group:before,
.form-group:after {
content: ' ';
display: table;
}
@media (max-width: 768px) {
.form-group {
margin-bottom: 15px;
}
}
.wrap-block {
width: 100%;
}
.wrapper .radius-btn {
display: inline-block;
color: $common-link;
margin: 0.9rem;
padding: 0.6rem 1rem;
background-color: $grey;
border: 1px solid $silver-grey;
border-radius: 1.5rem;
}
.wrapper .radius-btn:hover {
cursor: pointer;
color: $brand;
border: 1px solid $brand;
animation: jelly 0.5s;
}
@keyframes jelly {
0%,
100% {
transform: scale(1, 1);
}
25% {
transform: scale(0.9, 1.1);
}
50% {
transform: scale(1.1, 0.9);
}
75% {
transform: scale(0.95, 1.05);
}
}
.no-padding {
padding: 0;
}
.divider {
margin: 10px 0;
}
.mb-normal {
margin-bottom: 1.8rem;
}
/* ----------------------title font define---------------------- Start*/
.large-font {
font-size: 5rem;
}
.medium-font {
font-size: 3.8rem;
}
.small-font {
font-size: 3rem;
}
@media (max-width: 768px) {
.large-font {
font-size: 3.8rem;
}
.medium-font {
font-size: 3rem;
}
.small-font {
font-size: 2.56rem;
}
}
@media (max-width: 414px) {
.large-font {
font-size: 3.8rem;
}
.medium-font {
font-size: 2.88rem;
}
.small-font {
font-size: 2.56rem;
}
}
/* ----------------------title font define---------------------- End*/
.text-center {
text-align: center;
}
.blur-effect {
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
}
.heart-wrap {
position: relative;
display: flex;
align-items: center;
width: 2.5rem;
height: 2.5rem;
&:hover {
.circle {
display: block;
}
}
.circle {
display: none;
width: 3.5rem;
height: 3.5rem;
position: absolute;
top: -0.5rem;
left: -0.5rem;
background-color: rgba($color: $brand, $alpha: 0.2);
border-radius: 50%;
}
}
.relative {
position: relative;
}
================================================
FILE: src/assets/scss/frame.scss
================================================
.header {
font-weight: 500 !important;
.menu {
display: none;
position: absolute;
top: 0;
left: 0;
padding: 30px 20px;
width: $header-height;
height: $header-height;
}
}
// header menu
.menu span:after,
.menu span:before {
content: '';
position: absolute;
left: 0;
top: 9px;
}
.menu span:after {
top: 18px;
}
.menu span {
position: relative;
display: block;
}
.menu span,
.menu span:after,
.menu span:before {
width: 100%;
height: 3px;
background-color: #000;
backface-visibility: hidden;
transition: background-color 0.38s;
}
.menu-expand .menu span {
background-color: transparent;
}
.menu-expand .menu span:before {
transform: rotate(45deg) translate(2px, 2px);
}
.menu-expand .menu span:after {
transform: rotate(-45deg) translate(4px, -5px);
}
================================================
FILE: src/assets/scss/icon.scss
================================================
.icons {
width: 2.2rem;
height: 2.2rem;
margin: 0 0.88em;
cursor: pointer;
}
.icon-heart {
width: 2rem;
height: 2rem;
}
================================================
FILE: src/assets/scss/layout.scss
================================================
#app {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
width: 100%;
height: 100%;
}
.wrapper {
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.2);
padding-top: $header-height;
.panel-default {
.panel-body {
min-height: calc(100vh - 280px);
// 天空的颜色(ColorsOfSky),参见:https://uigradients.com/#ColorsOfSky
@include get-gradient-background(#efefef, #f4f5f5, top);
.main-container {
width: 100%;
max-width: 100rem;
margin: auto;
.entry-list {
position: relative;
display: inline-block;
float: left;
background-color: $white;
width: 66%;
.page-responsive {
padding: 15px 0;
}
.el-tabs__item,
.el-card__header {
font-size: $font-small;
}
}
.tip-box-card {
.el-card__body {
padding: 18px 20px !important;
.no-result-tip {
display: flex;
flex-direction: column;
align-items: center;
text-align: left;
font-size: $font-small;
color: $silver-grey;
line-height: 2rem;
.no-result-img {
width: 100%;
max-width: 500px;
margin-bottom: 1rem;
}
.no-result-tip-a {
color: $brand;
&:hover {
text-decoration: none;
color: $brand;
}
}
}
}
}
}
}
}
}
.nicelinks-logo {
display: inline-block;
color: $black;
width: 100%;
margin: 0;
text-align: center;
&:hover,
&:focus,
&:link,
&:active {
color: $black;
}
.title {
display: inline-block;
margin: 0 1rem;
font-size: $font-medium;
font-weight: 500;
text-align: center;
color: $black;
vertical-align: middle;
&:hover,
&:focus,
&:link,
&:active {
color: $black;
}
}
img {
width: 3.6rem;
}
}
.nav-menu-item {
display: inline-block;
float: right;
min-width: 72px;
height: 100%;
color: #ffffff;
font-size: 1.6em;
list-style-type: none;
margin: 0 10px;
}
.sidebar-aside {
margin-bottom: 3rem;
.aside-widget-title {
font-size: $font-medium;
font-weight: 800;
margin: 2rem 0;
padding: 0;
}
}
.nav-menu-item:before {
content: '';
display: inline-block;
vertical-align: middle;
height: 100%;
width: 0;
}
.table-operate {
margin-top: 10px;
}
.page-second-title {
font-size: $font-large;
color: $black;
text-align: left;
font-weight: 500;
margin-top: 3rem;
}
.text-ellipsis {
display: block;
overflow: hidden;
white-space: nowrap;
-o-text-overflow: ellipsis;
text-overflow: ellipsis;
}
/* Recommend Seal */
.recommend-seal {
position: absolute;
right: 20px;
top: -120px;
.seal {
position: relative;
width: 80px;
height: 80px;
transform: rotate(-60deg);
color: $brand;
display: grid;
place-content: center;
text-align: center;
font-size: 12px;
outline: 6px solid $brand;
outline-offset: 5px;
border: 3px solid $brand;
border-radius: 6px;
opacity: 0;
transform: rotate(-2deg) scale(2);
animation: seal 0.2s cubic-bezier(0.6, 0.04, 0.98, 0.335) forwards;
&::after {
content: '官方\A推荐';
white-space: pre;
line-height: 24px;
letter-spacing: 4px;
font-weight: 900;
font-size: 20px;
}
}
}
.offline-seal {
position: absolute;
right: 0;
top: -120px;
.seal {
display: flex;
justify-content: center;
align-items: center;
width: 160px;
height: 60px;
border: solid 4px $silver-grey;
border-radius: 0.2em;
color: $silver-grey;
content: 'Draft';
font-size: 50px;
font-weight: bold;
line-height: 1;
opacity: 0;
padding: 0.1em 0.5em;
text-transform: uppercase;
opacity: 0;
transform: rotate(-2deg) scale(2);
animation: seal 0.2s cubic-bezier(0.6, 0.04, 0.98, 0.335) forwards;
&::before {
position: absolute;
content: '';
inset: 3px;
border: 2px solid $silver-grey;
z-index: 0;
}
&::after {
content: '网站已离线';
font-weight: 900;
font-size: 20px;
}
}
}
@keyframes seal {
100% {
opacity: 0.8;
transform: rotate(-30deg) scale(1);
}
}
@media screen and (max-width: $mobile-screen) {
.recommend-seal {
top: -100px;
.seal {
width: 60px;
height: 60px;
outline: 4px solid $brand;
border: 2px solid $brand;
&::after {
line-height: 18px;
letter-spacing: 2px;
font-weight: 600;
font-size: 14px;
}
}
}
}
================================================
FILE: src/assets/scss/media.scss
================================================
.theme-jade-color {
background-color: #313538;
background-image: -webkit-radial-gradient(
center,
circle farthest-corner,
#555a5f 0%,
#1c1e20 100%
);
background-image: radial-gradient(circle farthest-corner at center, #555a5f 0%, #1c1e20 100%);
}
.button {
background: linear-gradient(to left, #abbaab, #ffffff);
}
@media screen and (max-width: $mobile-screen) {
.operate-tabs-space {
padding-top: 10rem;
}
.icons {
width: 2.5rem;
height: 2.5rem;
margin: 0 0.66em;
}
.form-group {
margin-bottom: 10px;
}
.button {
color: #111;
background-image: radial-gradient(circle farthest-corner at center, #eee 20%, #ccc 68%);
}
.theme-jade-color {
background-color: #dddddd;
background-image: -webkit-radial-gradient(center, circle farthest-corner, #fff 0%, #ddd 100%);
background-image: radial-gradient(circle farthest-corner at center, #fff 0%, #ddd 100%);
}
.wrapper {
padding-top: $header-mobile-height;
}
#app .menu-expand {
.menu {
}
.sidenav {
display: block !important;
}
}
#app .el-dialog--small {
width: 90%;
}
#app .wrapper .panel-body {
padding: 0px;
.main-container {
.entry-list {
display: block;
width: 100%;
float: none;
.links-list .moudle {
margin: 0px;
padding: 2.1rem 1rem;
}
}
.sidebar {
display: block;
width: 100%;
max-width: 36rem;
float: none;
margin: 2rem auto;
padding: 2rem;
border-bottom: 1px solid $moudle-border-color;
}
.page-responsive {
overflow: hidden;
}
.el-card__body {
padding: 15px 20px;
}
}
}
}
@media screen and (min-width: $mobile-screen) {
#app .menu-expand {
.menu {
}
.sidenav {
display: none;
}
}
}
.mobile-search {
position: fixed;
top: 60px;
left: 0;
width: 100%;
background-color: $white;
margin: 0;
padding: 10px 15px;
z-index: $zindex-mobile-search;
}
.mobile-search-serving {
margin-top: 60px;
}
.el-form-item {
margin-bottom: 0;
}
#app .el-dialog__headerbtn .el-dialog__close {
font-size: 24px;
}
================================================
FILE: src/assets/scss/mixins.scss
================================================
@mixin clearfix() {
&:before,
&:after {
content: ' ';
display: table;
}
&:after {
clear: both;
}
}
@mixin flex-box-center($direction: row, $justify: center, $align-items: center) {
flex-direction: $direction;
display: -webkit-box;
display: -moz-box;
display: -webkit-flex;
display: -ms-flex;
display: flex;
-webkit-box-align: center;
-moz-box-align: center;
-ms-box-align: center;
-webkit-align-items: $align-items;
-ms-align-items: $align-items;
align-items: $align-items;
-webkit-box-pack: center;
-webkit-justify-content: center;
justify-content: $justify;
}
// 根据参数声称渐变色:https://uigradients.com/#Moonrise
@mixin get-gradient-background($front, $back, $direction: bottom) {
background: $front;
background: -webkit-linear-gradient(to $direction, $front, $back);
background: linear-gradient(to $direction, $front, $back);
}
@mixin text-ellipsis-multiline($num: 3) {
display: -webkit-box;
-webkit-line-clamp: $num;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
================================================
FILE: src/assets/scss/style.scss
================================================
@import 'variables.scss';
@import 'mixins.scss';
@import 'common.scss';
@import 'frame.scss';
@import 'icon.scss';
@import 'layout.scss';
@import 'theme-element.scss';
@import 'media.scss';
@import 'colors.scss';
================================================
FILE: src/assets/scss/theme-element.scss
================================================
#app {
.el-input__inner,
.el-textarea__inner {
background-repeat: no-repeat;
background-position: right 8px center;
border: 1px solid $border-grey;
outline: none;
box-shadow: inset 0 1px 2px rgba(27, 31, 35, 0.075);
&:hover {
border: 1px solid $brand;
}
}
.el-input.is-disabled .el-input__inner {
border-color: rgb(209, 219, 229);
cursor: not-allowed;
}
.el-table th {
text-align: center;
}
.el-tag {
margin: auto 5px;
}
.el-table td,
.el-table th {
height: 60px;
}
.el-table .cell {
line-height: 60px;
height: 60px;
}
.el-badge__content.is-fixed {
top: 8px;
}
.el-button--mini {
padding: 0 4px;
border-radius: 0px;
min-width: 60px;
}
.el-card {
border: none;
.el-card__header {
text-align: left;
}
}
.box-card {
.form-group {
width: 100%;
min-height: 36px;
}
.clearfix:before,
.clearfix:after {
display: table;
content: '';
}
.clearfix:after {
clear: both;
}
.el-breadcrumb {
font-size: 1.8rem;
}
}
.el-menu {
background-color: $white;
.el-menu-item {
color: $black;
font-size: $font-small;
}
.el-submenu {
.el-submenu__title {
color: $black;
font-size: $font-small;
}
.el-menu-item {
color: $black;
}
.el-menu {
background-color: $grey;
}
}
}
.el-button--default {
&:hover,
&:focus,
&:active {
color: $brand;
cursor: pointer;
border: 1px solid $brand;
background-color: transparent;
}
}
.el-button--primary {
background-color: $brand;
border-color: $brand;
color: $white;
font-weight: 500;
&:hover,
&:focus,
&:active {
color: $white;
cursor: pointer;
animation: jelly 0.5s;
background-color: $brand;
}
}
.el-dropdown,
.el-tabs__item,
.el-button--text {
color: $black;
transition: color 100ms cubic-bezier(0.125, 0.565, 0.86, 0.31);
&:hover,
&:focus,
&:active {
color: $brand;
}
}
.el-tabs__active-bar {
background-color: $brand;
}
.el-tabs__item.is-active,
.el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
color: $brand;
}
.el-tabs__active-bar {
height: 2px;
}
.el-switch.is-checked .el-switch__core {
border-color: $brand;
background-color: $brand;
}
}
.el-message-box__wrapper {
.el-message-box {
width: 86%;
min-width: 300px;
max-width: 450px;
.el-message-box__header {
.el-message-box__title {
display: inline-block;
}
.el-message-box__close {
float: right;
}
}
}
}
.header {
.el-tabs__header,
.el-tabs__nav {
border-bottom: none !important;
}
.el-tabs__nav-wrap {
margin-bottom: 0px !important;
}
.el-tabs__nav {
background-color: #ffffff;
}
}
@keyframes jelly {
0%,
100% {
transform: scale(1, 1);
}
25% {
transform: scale(0.9, 1.1);
}
50% {
transform: scale(1.1, 0.9);
}
75% {
transform: scale(0.95, 1.05);
}
}
.v-modal {
animation: none !important;
}
================================================
FILE: src/assets/scss/variables.scss
================================================
$brand: #ef4136;
$white: #ffffff;
$black: #17223b;
$grey: #f7f8f9;
$red: #fa0101;
$jade: #34dfa5;
$producthunt: #ea552d;
$white-grey: #f4f6fa;
$silver-grey: #9393aa;
$black-grey: #272755;
$dropdown-grey: #606266;
$common-link: #48576a;
$common-link-hover: #263238;
$link-title: #272755;
$link-title-hover: $jade;
$input-placeholder: #888888;
$border-grey: #d1d5da;
$border-black: #7a8ba9;
$footer-grey: #969696;
$footer-grey-bg: #eaeaea;
$entry-btn-grey: #97a8be;
$entry-btn-hover: #333333;
$moudle-border-color: #d1d5da;
$item-border-color: #e4e7ed;
$panel-body-padding: 15px !default;
//----------------------- font size -----------------------
$font-large: 2rem;
$font-medium: 1.8rem;
$font-small: 1.6rem;
//----------------------- $z-index ------------------------
$zindex-subhead: 100;
$zindex-mobile-search: 1000;
$zindex-header-mobile: 2000;
$zindex-side-nav: 2000;
$zindex-quickapp-btn: 99999;
$zindex-elevator: 99999;
$zindex-upload-avatar: 100000;
$zindex-auto-dialog: 200000;
//--------------------- width height ----------------------
$mobile-screen: 960px;
$large-screen: 1560px;
$small-mobile-screen: 375px;
$tiny-mobile-screen: 320px;
$side-nav-width: 100%;
$header-height: 80px;
$header-mobile-height: 60px;
$footer-height: 240px;
$section-height: 32rem;
$crescent-height: 10rem;
================================================
FILE: src/components/CountUp.vue
================================================
<template>
<strong class="countup"></strong>
</template>
<script>
import CountUpJs from 'countup'
export default {
name: 'CountUp',
props: {
start: {
type: Number,
required: false,
default: 0,
},
end: {
type: Number,
required: true,
},
decimals: {
type: Number,
required: false,
default: 0,
},
duration: {
type: Number,
required: false,
default: 2,
},
options: {
type: Object,
required: false,
},
},
data() {
return {
instance: null,
}
},
computed: {},
watch: {
end: {
handler: function (value) {
if (this.instance && this.instance.update) {
this.instance.update(value)
}
},
deep: false,
},
},
methods: {
init() {
if (!this.instance) {
const dom = this.$el
this.instance = new CountUpJs(
dom,
this.start,
this.end,
this.decimals,
this.duration,
this.options
)
this.instance.start(() => {
this.$emit('callback', this.instance)
})
}
},
destroy() {
this.instance = null
},
},
mounted() {
this.init()
},
destroyed() {
this.destroy()
}
}
</script>
<style lang="scss" scoped>
.countup {
font-size: 8.8rem;
font-weight: 400;
background: #212121;
-webkit-background-clip: text;
-moz-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 3px 3px rgba(255, 255, 255, 0.5);
}
</style>
================================================
FILE: src/components/Elevator.vue
================================================
<template>
<div class="elevator">
<div class="tooltip">
<div class="pannel">
<div class="item">
<img
class="qrcode"
src="https://image.nicelinks.site/qrcode_jqx.jpg"
alt="晚晴幽草轩-公众号"
/>
<span class="font-medium text">晚晴幽草轩</span>
<span class="text">微信扫码关注</span>
</div>
<div class="item">
<img
class="qrcode"
src="https://image.nicelinks.site/nicelinks-miniprogram-code.jpeg?imageView2/1/w/250/h/250/interlace/1/ignore-error/1"
alt="倾城之链-小程序"
/>
<span class="font-medium text">倾城之链</span>
<span class="text">微信扫码体验</span>
</div>
</div>
<div class="connect outside-link">
<icon class="qrcode" name="qrcode"></icon>
</div>
</div>
<a
target="_blank"
class="outside-link"
@click="onRecommendClick"
rel="noopener"
:href="reportPath"
>投稿
</a>
<a
target="_blank"
class="outside-link"
@click="onFeedbackClick"
rel="noopener"
:href="reportPath"
>反馈
</a>
</div>
</template>
<script>
import { REPORT_PATH } from 'config/constant'
export default {
name: 'Feedback',
data() {
return {
reportPath: REPORT_PATH,
}
},
methods: {
onRecommendClick() {
this.$gtagTracking('recommend-btn', 'elevator')
},
onFeedbackClick() {
this.$gtagTracking('feedback-btn', 'elevator')
},
},
}
</script>
<style scoped lang="scss">
@import './../assets/scss/variables.scss';
$factor: 1rem;
.elevator {
position: fixed;
top: 50%;
right: 3 * $factor;
transform: translateY(-50%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: $zindex-elevator;
}
.outside-link {
display: flex;
justify-content: center;
align-items: center;
width: 5 * $factor;
height: 5 * $factor;
border-radius: 50%;
z-index: 999;
text-align: center;
font-weight: 900;
font-size: inherit;
font-family: inherit;
color: $black;
background-color: $white;
word-break: keep-all;
padding: 0.5em 1em;
margin: 1rem 0;
outline: none;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
&:hover {
cursor: pointer;
animation: jelly 0.5s;
}
}
.elevator .tooltip {
position: relative;
display: inline-block;
&:hover {
.pannel {
visibility: visible;
}
}
.pannel {
position: absolute;
bottom: -9.25 * $factor;
right: 6 * $factor;
z-index: 100;
width: 40 * $factor;
height: 26 * $factor;
visibility: hidden;
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
background-color: $white;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
padding: 2 * $factor;
border-radius: 2 * $factor;
&:before {
content: '';
position: absolute;
left: 100%;
top: 12.5 * $factor;
width: 0;
height: 0;
border-top: $factor solid transparent;
border-left: $factor solid $white;
border-bottom: $factor solid transparent;
}
.item {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 18 * $factor;
height: 100%;
.qrcode {
width: 16 * $factor;
height: 16 * $factor;
aspect-ratio: 1 / 1;
}
.text {
font-size: 1.6 * $factor;
color: $common-link;
}
}
}
.connect {
.qrcode {
width: 3 * $factor !important;
height: 3 * $factor !important;
margin: 0 auto;
transform: scale(1);
}
}
}
@keyframes jelly {
0%,
100% {
transform: scale(1, 1);
}
25% {
transform: scale(0.9, 1.1);
}
50% {
transform: scale(1.1, 0.9);
}
75% {
transform: scale(0.95, 1.05);
}
}
</style>
================================================
FILE: src/components/Heart.vue
================================================
<template functional>
<div class="heart-wrap">
<div class="circle"></div>
<svg v-if="props.isDown" class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24"
height="24">
<path
d="M923.022 283.591a260.01 260.01 0 0 0-56.917-82.773 265.33 265.33 0 0 0-186.397-75.805A267.093 267.093 0 0 0 512 184.064a267.093 267.093 0 0 0-167.708-59.108c-35.5 0-69.888 6.827-102.4 20.31a263.85 263.85 0 0 0-83.997 55.495 258.446 258.446 0 0 0-77.91 184.718c0 33.28 6.827 67.982 20.31 103.282 11.293 29.497 27.506 60.103 48.213 91.022 32.797 48.868 77.881 99.869 133.888 151.609 92.815 85.675 184.719 144.868 188.587 147.285l23.723 15.19c10.496 6.713 23.978 6.713 34.503 0l23.694-15.19c3.897-2.503 95.687-61.61 188.587-147.313 56.035-51.712 101.12-102.685 133.916-151.61 20.68-30.89 36.978-61.496 48.185-90.993 13.511-35.271 20.31-69.973 20.31-103.31a254.578 254.578 0 0 0-20.907-101.888z"
fill="#ef4136" fill-opacity="1" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-heart" width="22" height="22"
viewBox="0 0 24 24" stroke-width="1.5" stroke="#9393aa" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
</svg>
</div>
</template>
================================================
FILE: src/components/HeartBroken.vue
================================================
<template functional>
<div class="heart-wrap">
<svg
v-if="props.isDown"
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-heart-broken"
width="22"
height="22"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="#ff2825"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
<path d="M12 7l-2 4l4 3l-2 4v3" />
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-heart-broken"
width="22"
height="22"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="#9393aa"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19.5 13.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" />
<path d="M12 7l-2 4l4 3l-2 4v3" />
</svg>
</div>
</template>
================================================
FILE: src/components/Icon/Icon.vue
================================================
<template>
<svg class="icons" :class="iconClass">
<use :xlink:href="Icons[name]"></use>
</svg>
</template>
<script>
import Icons from 'src/assets/icons'
export default {
props: {
name: {
type: String,
required: true,
default: '',
validator(val) {
return Icons[val]
},
},
},
data() {
return {
Icons: Icons,
iconClass: 'icon-' + this.name,
}
},
}
</script>
================================================
FILE: src/components/Icon/index.js
================================================
module.exports = require('./Icon.vue')
================================================
FILE: src/components/LoadMore.vue
================================================
<template>
<div class="load-more">
<el-button
type="primary"
icon="plus"
size="large"
v-if="isShowLoadMore"
@click="onLoadMoreClick"
>{{ $t('loadMoreStr') }}
</el-button>
<el-alert v-else title="嘿,云集美好,我是有底线的" type="info"> </el-alert>
</div>
</template>
<script>
import partsMixin from 'mixins/partsMixin.js'
export default {
name: 'LoadMore',
mixins: [partsMixin],
computed: {
isShowLoadMore() {
return this.$store && this.$store.state.isLoadMore
},
},
methods: {
onLoadMoreClick() {
const pageCount = this.$requestParamList.pageCount + 1
this.$fetchSearch({ pageCount }, true)
},
}
}
</script>
<style lang="scss" scoped>
@import './../assets/scss/variables.scss';
.load-more {
width: 70%;
margin: auto;
padding: 2rem 0;
.el-button {
color: $black;
background-color: #ffffff;
border-radius: 20px;
border: 1px solid $entry-btn-grey;
width: 80%;
&:hover {
border: 1px solid $entry-btn-hover;
}
}
.el-alert {
border-radius: 20px;
}
}
</style>
================================================
FILE: src/components/OfflineSeal.vue
================================================
<template functional>
<div class="offline-seal">
<div class="seal">
</div>
</div>
</template>
================================================
FILE: src/components/OperateTabs.vue
================================================
<template>
<div class="operate-tabs" id="operate-tabs">
<div class="nav-wrap">
<a
v-for="item in items"
class="link"
:class="{ active: item === activeName }"
@click="onTabsClick(item)"
:href="getAssembleRoute(item)"
:key="item"
>{{ $t(item) }}
</a>
</div>
</div>
</template>
<script>
export default {
name: 'OperateTabs',
data() {
return {
activeName: 'latest',
}
},
computed: {
items() {
return ['latest', 'hottest', 'earliest']
},
},
mounted() {
this.activeName = this.$route.query.sort || this.activeName
},
methods: {
getAssembleRoute(sort) {
const path = this.$route.path
return `${path}?sort=${sort}`
},
onTabsClick(item) {
this.$gtagTracking(`explore-tabs-${item}`, 'explore')
this.$adsConversionReport('explore-tabs')
}
}
}
</script>
<style lang="scss">
@import './../assets/scss/variables.scss';
@import '../assets/scss/mixins.scss';
.operate-tabs {
padding-top: 15px;
.nav-wrap {
@include flex-box-center(row, start, center);
overflow: hidden;
margin-bottom: -1px;
:first-child {
border-top-left-radius: 0.5rem;
}
:last-child {
border-top-right-radius: 0.5rem;
}
.link {
font-weight: 500;
color: $link-title;
padding: 1rem 1.5rem;
border: 1px solid $moudle-border-color;
}
.link + .link {
border-left: 0;
}
.active {
color: $brand;
border-bottom-color: $white;
}
}
}
</style>
================================================
FILE: src/components/Pagination.vue
================================================
<template>
<div class="pagination" v-if="items.length > 0">
<ul class="pager">
<li class="item" :class="{ disabled: item === '...' }" v-for="item in items">
<a class="link" :class="{ active: item === page }" :href="getLinkPath(item)">{{ item }}</a>
</li>
<li class="item" style="width: 7rem;" v-if="isNext">
<a class="link" :href="getLinkPath(page + 1)">下一页</a>
</li>
</ul>
</div>
</template>
<script>
import { PAGE_SIZE } from 'config/constant'
const PAGE_LIMIT = 6
export default {
name: 'Pagination',
props: {
count: {
type: Number,
default: 10,
},
page: {
type: Number,
default: 1,
},
},
computed: {
isNext() {
const length = Math.ceil(this.count / PAGE_SIZE)
return this.page < length
},
items() {
const length = Math.ceil(this.count / PAGE_SIZE)
if (length <= 1) return []
if (length <= PAGE_LIMIT) {
return this.range(1, length)
}
const maxLength = Math.min(length, PAGE_LIMIT)
const even = maxLength % 2 === 0 ? 1 : 0
const left = Math.floor(maxLength / 2)
const right = length - left + 1 + even
if (this.page > left && this.page < right) {
const firstItem = 1
const lastItem = length
const start = this.page - left + 2
const end = this.page + left - 2 - even
const secondItem = start - 1 === firstItem + 1 ? 2 : '...'
const beforeLastItem = end + 1 === lastItem - 1 ? end + 1 : '...'
return [1, secondItem, ...this.range(start, end), beforeLastItem, length]
} else if (this.page === left) {
const end = this.page + left - 1 - even
return [...this.range(1, end), '...', length]
} else if (this.page === right) {
const start = this.page - left + 1
return [1, '...', ...this.range(start, length)]
} else {
return [...this.range(1, left), '...', ...this.range(right, length)]
}
},
},
methods: {
range(from, to) {
const range = []
from = from > 0 ? from : 1
for (let i = from; i <= to; i++) {
range.push(i)
}
return range
},
getLinkPath(page) {
page = parseInt(page)
if (!page) return
const path = this.$route.path
const sort = this.$route.query.sort || 'latest'
const pills = sort === 'latest' ? '' : `sort=${sort}&`
return `${path}?${pills}page=${page}`
},
},
}
</script>
<style lang="scss">
@import './../assets/scss/variables.scss';
@import '../assets/scss/mixins.scss';
.pagination {
@include flex-box-center(column, center, center);
padding: 2rem 0;
.pager {
@include flex-box-center(row, center, center);
:first-child {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
:last-child {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
.item {
width: 5rem;
height: 5rem;
border: 1px solid $moudle-border-color;
list-style: none;
&:hover {
background-color: $white-grey;
}
.link {
@include flex-box-center(row, center, center);
width: 100%;
height: 100%;
color: $link-title;
font-size: $font-medium;
font-weight: 500;
}
.active {
pointer-events: none;
cursor: not-allowed;
border-radius: 0;
color: $white;
background-color: $brand;
}
}
.item + .item {
border-left: 0;
}
.disabled {
cursor: not-allowed;
filter: alpha(opacity=65);
-webkit-box-shadow: none;
box-shadow: none;
opacity: 0.65;
background-color: $border-grey;
}
}
}
@media screen and (max-width: $mobile-screen) {
.pagination .pager .item {
width: 4rem;
height: 4rem;
}
}
</style>
================================================
FILE: src/components/RecommendSeal.vue
================================================
<template functional>
<div class="recommend-seal">
<div class="seal">
</div>
</div>
</template>
================================================
FILE: src/components/Search.vue
================================================
<template>
<el-autocomplete id="search-nice" popper-class="search-autocomplete" placement="bottom-start"
:select-when-unmatched="isSelectWhenUnmatched" :trigger-on-focus="isTriggerFocus"
:popper-append-to-body="isPopperToBody" v-model="keyword" :fetch-suggestions="handleFetchNiceLinks"
placeholder="搜您想要,探索美好" @select="handleSearchSelect" @focus="handleSearchFocus" @blur="handleSearchBlur">
<i class="el-icon-search el-input__icon" slot="suffix" @click="handleIconClick"> </i>
<template slot-scope="{ item }">
<p class="item-title" v-html="styleForTitle(item)"></p>
<div class="item-desc" v-html="styleForDesc(item)"></div>
</template>
</el-autocomplete>
</template>
<script>
import Vue from 'vue'
import { Autocomplete } from 'element-ui'
import 'element-ui/lib/theme-chalk/autocomplete.css'
import debounce from 'lodash/debounce'
import { parse } from './../helper/marked'
import { filterHtmlTag, sliceToAheadTarget } from './../helper/tool'
import { isAndroidSystem } from './../helper/system'
Vue.use(Autocomplete)
export default {
name: 'Search',
data() {
return {
keyword: '',
keywordBackup: '',
isSelectWhenUnmatched: true,
isTriggerFocus: true,
isPopperToBody: true,
}
},
methods: {
requestSearchTarget(queryString = '', callback) {
return debounce(() => {
this.$apis
.searchNiceLinks({
keyword: queryString,
})
.then((result) => {
callback(result)
})
.catch((error) => {
this.$message.error(`${error}`)
})
}, 600)
},
handleFetchNiceLinks(queryString, callback) {
this.keywordBackup = queryString
if (queryString === '' && queryString.trim() === '') {
const defSize = this.$isMobile ? 8 : 16
return this.$apis
.getRandomLinks({ size: defSize })
.then((result) => {
callback(result)
})
.catch((error) => {
this.$message.error(`${error}`)
})
}
// 增加防抖功效,提升检索体验 @2022.03.16
this.requestSearchTarget(queryString, callback)()
},
handleSearchSelect(item) {
if (!item || !item._id) return
const keyword = this.keywordBackup || 'srandom'
const stype = item.stype || 'srandom'
const paramStr = `?keyword=${keyword}&type=${stype}`
this.$router.push(`/post/${item._id}${paramStr}`)
},
handleIconClick() { },
handleSearchFocus() {
if (this.$isMobile) {
if (isAndroidSystem()) {
document.querySelector('.search-autocomplete').style.display = 'none'
setTimeout(() => {
document.querySelector('.search-autocomplete').style.display = 'block'
document.querySelector('.search-autocomplete').style.top = '110px'
}, 360)
}
return
}
const expandedWidth = window.innerWidth > 1560 ? 666 : 521
document.querySelector('.el-input__inner').style.width = `${expandedWidth}px`
},
handleSearchBlur() {
if (this.$isMobile) return
document.querySelector('.el-input__inner').style.width = '221px'
},
styleForTitle(item) {
return item.title.replace(this.keyword, `<i class="keyword">${this.keyword}</i>`)
},
styleForDesc(item) {
const tempDescText = item && item.stype ? item[item.stype] : item.desc || item.review
let descTextStr = parse(tempDescText)
descTextStr = filterHtmlTag(descTextStr)
descTextStr = sliceToAheadTarget(descTextStr, this.keyword)
return descTextStr.replace(this.keyword, `<i class="keyword">${this.keyword}</i>`)
},
},
}
</script>
<style lang="scss">
@import '../assets/scss/variables.scss';
@import '../assets/scss/mixins.scss';
.el-autocomplete-suggestion {
.el-scrollbar {
.el-autocomplete-suggestion__wrap {
height: 49rem;
min-height: 49rem;
}
.el-autocomplete-suggestion__list li {
border-bottom: 1px dashed $border-grey;
}
}
}
@media (min-width: $large-screen) {
.el-autocomplete-suggestion {
.el-scrollbar {
.el-autocomplete-suggestion__wrap {
height: 618px;
min-height: 618px;
}
}
}
}
@media screen and (max-width: $mobile-screen) {
.el-autocomplete-suggestion {
top: 110px !important;
.el-scrollbar {
.el-autocomplete-suggestion__wrap {
height: 34rem;
min-height: 34rem;
}
}
}
}
@media screen and (max-width: $tiny-mobile-screen) {
.el-autocomplete-suggestion {
top: 110px !important;
.el-scrollbar {
.el-autocomplete-suggestion__wrap {
height: 28.6rem;
min-height: 28.6rem;
}
}
}
}
.search-autocomplete .el-autocomplete-suggestion__wrap {
li {
padding: 0 15px;
line-height: 18px;
.item-title {
font-weight: 400;
font-size: $font-medium;
line-height: 18px;
padding: 1rem 0;
color: $black;
text-overflow: ellipsis;
overflow: hidden;
}
.item-desc {
display: block;
line-height: 18px;
padding: 1rem 0;
color: $silver-grey;
font-size: $font-small;
text-overflow: ellipsis;
overflow: hidden;
}
}
}
.el-autocomplete .el-input--suffix .el-input__inner {
font-size: $font-small;
width: 100%;
border-radius: 20px;
}
.keyword {
font-style: normal;
color: $brand;
text-decoration: underline;
}
#search-nice.search-extra-class {
top: 0px;
opacity: 0;
animation-name: search-up-hide;
animation-fill-mode: both;
animation-duration: 0.3s;
animation-timing-function: ease-out;
}
@keyframes search-up-hide {
0% {
top: 60px;
}
100% {
display: none;
}
}
.sub-head-follow {
top: 60px;
animation-name: sub-head-up;
animation-fill-mode: both;
animation-duration: 0.3s;
animation-timing-function: ease-out;
}
@keyframes sub-head-up {
0% {
top: 110px;
}
100% {
top: 60px;
}
}
</style>
================================================
FILE: src/components/SimilarRecommend.vue
================================================
<template>
<div class="similar-recommend" v-if="linksArr.length > 0">
<h2 class="page-second-title">猜您可能喜欢</h2>
<div class="list-item" v-for="item in linksArr" :key="item._id">
<router-link
@click.native="onSimilarClick"
class="recommend-link"
:to="getAssembleRoute(item)"
>
<div class="screenshot">
<div class="image-placeholder" v-show="isShowPlaceholder">
<strong>图片加载中...</strong>
</div>
<img
v-show="!isShowPlaceholder"
class="image"
:alt="getImgAlt(item)"
:src="getScreenshotPath(item)"
onerror="javascript:this.src='https://oss.nicelinks.site/nicelinks.site.png';"
/>
</div>
<div class="meta">
<h3 class="title">{{ getAssembleTitle(item) }}</h3>
<div class="desc" style="-webkit-box-orient: vertical;">{{ getAssembleDesc(item) }}</div>
</div>
</router-link>
</div>
</div>
</template>
<script>
import { getHostnameByUrl, interceptString } from './../helper/tool'
import { NICE_LINKS, NICE_LINKS_NAME, DESCRIPTION } from './../config/constant'
import { parse } from 'helper/marked'
const DEFAULT_LINKS_ARR = []
;[0, 1, 2, 3, 4].map(() => {
DEFAULT_LINKS_ARR.push({
urlPath: NICE_LINKS,
item: NICE_LINKS_NAME,
desc: DESCRIPTION,
_id: '5aa2579e56ee0d60651820c5',
review: DESCRIPTION,
})
})
export default {
name: 'SimilarRecommend',
data() {
return {
linksArr: Object.freeze(DEFAULT_LINKS_ARR),
RECOMMEND_NUM: 5,
isShowPlaceholder: true,
}
},
props: {
pdata: {
type: [Array, Object],
default: () => {
return []
},
},
},
watch: {
pdata() {
this.fetchSimilarTagLinks()
},
},
mounted() {
setTimeout(() => {
this.isShowPlaceholder = false
}, 300)
},
methods: {
assembleSimilarLinks(resArr) {
const tempArr = resArr.filter((item) => {
return item._id !== this.pdata._id
})
this.linksArr = Object.freeze(tempArr.slice(0, this.RECOMMEND_NUM))
},
fetchSimilarTagLinks() {
this.$apis
.getLinksByTag({
active: true,
alive: true,
pageCount: 1,
pageSize: this.RECOMMEND_NUM + 1,
sortType: 1,
sortTarget: 'created',
tags: this.pdata.tags[0],
})
.then((res) => {
this.assembleSimilarLinks(res)
})
.catch((error) => {
console.error(`Something Error @fetchSimilarTagLinks:`, error)
})
.finally(() => {})
},
getImgAlt(item) {
return `${item.title} - ${NICE_LINKS_NAME}`
},
getAssembleTitle(item) {
return interceptString(item.title)
},
getAssembleDesc(item) {
const content = parse(item.review).replace(/<[^>]*>/g, '') || item.desc
return interceptString(content)
},
getAssembleRoute(item) {
return `/post/${item._id}`
},
getScreenshotPath(item) {
const urlPath = getHostnameByUrl(item.urlPath)
return `https://oss.nicelinks.site/${urlPath}.png?x-oss-process=style/png2jpg&imageView2/1/w/320/h/180/interlace/1/ignore-error/1`
},
onSimilarClick() {
this.$gtagTracking('similar-recommend', 'post')
},
},
}
</script>
<style lang="scss" scoped>
@import '../assets/scss/variables.scss';
@import '../assets/scss/mixins.scss';
.similar-recommend {
width: 100%;
padding: 0 2rem;
.list-item {
width: 100%;
height: 9rem;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
margin: 1.2rem auto;
box-sizing: content-box;
@include flex-box-center(column, center, inherit);
.screenshot {
position: relative;
width: 20%;
height: 6rem;
.image-placeholder {
position: absolute;
top: 0;
left: 0;
@include flex-box-center(row, center, center);
width: 100%;
aspect-ratio: 16 / 9;
background-color: $white-grey;
color: $silver-grey;
}
.image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.recommend-link {
@include flex-box-center(row, space-between, center);
height: 100%;
padding: 1rem;
}
.meta {
width: 78%;
height: 100%;
@include flex-box-center(column, space-between, flex-start);
text-align: left;
.title {
width: 100%;
font-size: $font-medium;
line-height: $font-large;
color: $link-title;
margin: 0;
padding: 0;
font-weight: 500;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.desc {
@include text-ellipsis-multiline(2);
color: #9393aa;
font-size: 1.6rem;
line-height: 1.8rem;
word-spacing: 2px;
}
}
}
}
@media (max-width: 768px) {
.similar-recommend .list-item {
height: 10rem;
}
}
</style>
================================================
FILE: src/components/SubHead.vue
================================================
<template>
<div class="sub-head" id="sub-head">
<ul class="sub-head-nav" ref="subHeadNav">
<li :class="makeClassName(null)">
<router-link @click.native="onAllTheme" to="/explore/all" class="theme-link">
{{ $t('all') }}
</router-link>
</li>
<li v-for="item in themeList" :key="item.vaule" :class="makeClassName(item)">
<router-link
@click.native="onExploreTheme(item)"
class="theme-link"
:to="getLinkPathByThemeVal(item.value)"
>
{{ item.key }}
</router-link>
</li>
</ul>
</div>
</template>
<script>
import partsMixin from 'mixins/partsMixin.js'
export default {
name: 'SubHead',
mixins: [partsMixin],
props: {
themeList: {
type: [Array],
default: () => [],
},
},
methods: {
isCurrentThemeVal(value) {
const cTheme = this.$route.params.theme || ''
return cTheme.toUpperCase() === value.toUpperCase()
},
makeClassName(item) {
if (!item) {
const cTheme = this.$route.params.theme
return !cTheme ? 'nav-item is-active' : 'nav-item'
}
const isTheTheme = this.isCurrentThemeVal(item.value)
return isTheTheme ? 'nav-item is-active' : 'nav-item'
},
getLinkPathByThemeVal(value) {
this.$vuexSetRequestParamList({
sortTarget: 'created',
sortType: -1,
})
return value ? `/theme/${value.toLocaleLowerCase()}` : '/explore/all'
},
onAllTheme() {
this.$gtagTracking('all-theme', 'explore')
},
onExploreTheme(item) {
this.$gtagTracking(`sub-${item.value}`, 'explore')
this.$adsConversionReport('from-explore-theme')
},
/* ------------变更 SubHead ”按钮“触发后展示方案(18-07-01)------------ */
/*
onThemeTagClick (value = '') {
if (value === '') {
return this.$router.push('/explore/all')
}
this.$switchRouteByTheme(value || '')
// let activeItem = document.querySelector('.sub-head-nav .is-active')
// this.$document.removeClass(activeItem, 'is-active')
// let subHeadNav = this.$refs['subHeadNav']
// let navItem = subHeadNav.querySelectorAll('.nav-item')
// this.$document.addClass(navItem[index], 'is-active')
// this.$emit('fetch-search', { theme: value })
// const parameters = {behavior: 'smooth', block: 'start', inline: 'start'}
// document.getElementById('nice-links').scrollIntoView(parameters)
} */
},
}
</script>
<style lang="scss">
@import '../assets/scss/variables.scss';
.sub-head {
width: 100%;
max-width: 960px;
overflow: auto;
text-align: left;
border-bottom: solid 1px #d1d5da;
padding: 0.3rem;
z-index: $zindex-subhead;
background-color: $white;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
transition: all 0.2s;
transform: translateZ(0);
&::-webkit-scrollbar {
background: transparent;
height: 0px;
}
&:hover::-webkit-scrollbar {
background: transparent;
height: 0px;
}
.sub-head-nav {
width: 100%;
display: flex;
margin: 0;
.nav-item {
margin: auto 0.5rem;
.theme-link {
display: inline-block;
min-width: 4rem;
padding: 1rem 0;
color: $black-grey;
font-size: $font-small;
font-weight: 500;
&:hover {
color: $brand;
}
}
}
.is-active {
.theme-link {
color: $brand;
}
}
}
}
@media screen and (max-width: $mobile-screen) {
.sub-head {
position: fixed;
top: 110px;
max-width: $mobile-screen;
background-color: $white;
z-index: $zindex-subhead;
}
}
</style>
================================================
FILE: src/components/SwitchTheme.vue
================================================
<template>
<div class="toggle-wrapper">
<div class="gg-sun btn" @click="onThemeToggle" v-if="isDarkMode"></div>
<div class="gg-moon btn" @click="onThemeToggle" v-else></div>
</div>
</template>
<script>
const DARK = 'dark'
const THEME_MODE = 'theme-mode'
export default {
data() {
return {
isDarkMode: false,
}
},
watch: {
isDarkMode(val) {
document.body.setAttribute('theme', val ? DARK : 'light')
},
},
mounted() {
const mode = window.localStorage.getItem(THEME_MODE)
this.isDarkMode = mode === DARK
},
methods: {
onThemeToggle() {
this.isDarkMode = !this.isDarkMode
const mode = this.isDarkMode ? 'dark' : 'light'
window.localStorage.setItem(THEME_MODE, mode)
this.$gtagTracking('theme-mode', 'header', mode)
},
},
}
</script>
<style lang="scss">
@import './../assets/scss/mixins.scss';
.toggle-wrapper {
@include flex-box-center(row, center, center);
width: 3rem;
height: 100%;
margin-left: 2rem;
.btn {
cursor: pointer;
}
// moon
.gg-moon,
.gg-moon::after {
display: block;
box-sizing: border-box;
border-radius: 50%;
}
.gg-moon {
overflow: hidden;
position: relative;
transform: rotate(-135deg) scale(var(--ggs, 1));
width: 20px;
height: 20px;
border: 2px solid #17223b;
border-bottom-color: transparent;
}
.gg-moon::after {
content: '';
position: absolute;
width: 12px;
height: 18px;
border: 2px solid transparent;
box-shadow: 0 0 0 2px;
top: 8px;
left: 2px;
}
// sun
.gg-sun {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs, 1));
width: 24px;
height: 24px;
background: linear-gradient(to bottom, currentColor 4px, transparent 0) no-repeat 5px -6px/2px 6px,
linear-gradient(to bottom, currentColor 4px, transparent 0) no-repeat 5px 14px/2px 6px,
linear-gradient(to bottom, currentColor 4px, transparent 0) no-repeat -8px 5px/6px 2px,
linear-gradient(to bottom, currentColor 4px, transparent 0) no-repeat 14px 5px/6px 2px;
border-radius: 100px;
box-shadow: inset 0 0 0 2px;
border: 6px solid transparent;
}
.gg-sun::after,
.gg-sun::before {
content: '';
display: block;
box-sizing: border-box;
position: absolute;
width: 24px;
height: 2px;
border-right: 4px solid;
border-left: 4px solid;
left: -6px;
top: 5px;
}
.gg-sun::before {
transform: rotate(-45deg);
}
.gg-sun::after {
transform: rotate(45deg);
}
}
</style>
================================================
FILE: src/components/UploadAvatar.vue
================================================
<template>
<div class="vue-image-crop-upload" v-if="value">
<div class="vicp-wrap">
<div class="vicp-close" @click="off">
<i class="vicp-icon4"></i>
</div>
<div class="vicp-step1" v-show="step === 1">
<div
class="vicp-drop-area"
@dragleave="preventDefault"
@dragover="preventDefault"
@dragenter="preventDefault"
@click="handleClick"
@drop="handleChange"
>
<i class="vicp-icon1" v-show="loading !== 1">
<i class="vicp-icon1-arrow"></i>
<i class="vicp-icon1-body"></i>
<i class="vicp-icon1-bottom"></i>
</i>
<span class="vicp-hint" v-show="loading !== 1">{{ lang.hint }}</span>
<span class="vicp-no-supported-hint" v-show="!isSupported">{{ lang.noSupported }}</span>
<input
type="file"
v-show="false"
v-if="step === 1"
@change="handleChange"
ref="fileinput"
/>
</div>
<div class="vicp-error" v-show="hasError"><i class="vicp-icon2"></i> {{ errorMsg }}</div>
<div class="vicp-operate">
<a @click="off" @mousedown="ripple">{{ lang.btn.off }}</a>
</div>
</div>
<div class="vicp-step2" v-if="step === 2">
<div class="vicp-crop">
<div class="vicp-crop-left" v-show="true">
<div class="vicp-img-container">
<img
:src="sourceImgUrl"
:style="sourceImgStyle"
class="vicp-img"
draggable="false"
@drag="preventDefault"
@dragstart="preventDefault"
@dragend="preventDefault"
@dragleave="preventDefault"
@dragover="preventDefault"
@dragenter="preventDefault"
@drop="preventDefault"
@touchstart="imgStartMove"
@touchmove="imgMove"
@touchend="createImg"
@touchcancel="createImg"
@mousedown="imgStartMove"
@mousemove="imgMove"
@mouseup="createImg"
@mouseout="createImg"
ref="img"
/>
<div class="vicp-img-shade vicp-img-shade-1" :style="sourceImgShadeStyle"></div>
<div class="vicp-img-shade vicp-img-shade-2" :style="sourceImgShadeStyle"></div>
</div>
<div class="vicp-range">
<input
type="range"
:value="scale.range"
step="1"
min="0"
max="100"
@input="zoomChange"
/>
<i
@mousedown="startZoomSub"
@mouseout="endZoomSub"
@mouseup="endZoomSub"
class="vicp-icon5"
></i>
<i
@mousedown="startZoomAdd"
@mouseout="endZoomAdd"
@mouseup="endZoomAdd"
class="vicp-icon6"
></i>
</div>
</div>
<div class="vicp-crop-right" v-show="true">
<div class="vicp-preview">
<div class="vicp-preview-item">
<img :src="createImgUrl" :style="previewStyle" :alt="$t('niceLinksStr')" />
<span>{{ lang.preview }}</span>
</div>
<div class="vicp-preview-item" v-if="!noCircle">
<img :src="createImgUrl" :style="previewStyle" :alt="$t('niceLinksStr')" />
<span>{{ lang.preview }}</span>
</div>
</div>
</div>
</div>
<div class="vicp-operate">
<a @click="setStep(1)" @mousedown="ripple">{{ lang.btn.back }}</a>
<a class="vicp-operate-btn" @click="prepareUpload" @mousedown="ripple">{{
lang.btn.save
}}</a>
</div>
</div>
<div class="vicp-step3" v-if="step === 3">
<div class="vicp-upload">
<span class="vicp-loading" v-show="loading === 1">{{ lang.loading }}</span>
<div class="vicp-progress-wrap">
<span class="vicp-progress" v-show="loading === 1" :style="progressStyle"></span>
</div>
<div class="vicp-error" v-show="hasError"><i class="vicp-icon2"></i> {{ errorMsg }}</div>
<div class="vicp-success" v-show="loading === 2">
<i class="vicp-icon3"></i> {{ lang.success }}
</div>
</div>
<div class="vicp-operate">
<a @click="setStep(2)" @mousedown="ripple">{{ lang.btn.back }}</a>
<a @click="off" @mousedown="ripple">{{ lang.btn.close }}</a>
</div>
</div>
<canvas v-show="false" :width="width" :height="height" ref="canvas"></canvas>
</div>
</div>
</template>
<script>
import uploadAvatar from 'helper/uploadAvatar.js'
let langConfig = uploadAvatar.getLangConf()
let mimesConf = {
jpg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
svg: 'image/svg+xml',
psd: 'image/photoshop',
}
export default {
name: 'UploadAvatar',
props: {
// 域,上传文件name,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
field: {
type: String,
default: 'avatar',
},
// 原名key,类似于id,触发事件会带上(如果一个页面多个图片上传控件,可以做区分
ki: {
default: 0,
},
// 显示该控件与否
value: {
default: true,
},
// 上传地址
url: {
type: String,
default: '',
},
// 其他要上传文件附带的数据,对象格式
params: {
type: Object,
default: null,
},
// Add custom headers
headers: {
type: Object,
default: null,
},
// 剪裁图片的宽
width: {
type: Number,
default: 200,
},
// 剪裁图片的高
height: {
type: Number,
default: 200,
},
// 不预览圆形图片
noCircle: {
type: Boolean,
default: false,
},
// 单文件大小限制
maxSize: {
type: Number,
default: 10240,
},
// 语言类型
langType: {
type: String,
default: 'zh',
},
// 语言包
langExt: {
type: Object,
default: null,
},
// 图片上传格式
imgFormat: {
type: String,
default: 'png',
},
},
data() {
let that = this,
{ imgFormat, langType, langExt, width, height } = that,
isSupported = true,
allowImgFormat = ['jpg', 'png'],
tempImgFormat = allowImgFormat.indexOf(imgFormat) === -1 ? 'jpg' : imgFormat,
lang = langConfig[langType] ? langConfig[langType] : langConfig['en'],
mime = mimesConf[tempImgFormat]
// 规范图片格式
that.imgFormat = tempImgFormat
if (langExt) {
Object.assign(lang, langExt)
}
if (typeof FormData !== 'function') {
isSupported = false
}
return {
// 图片的mime
mime,
// 语言包
lang,
// 浏览器是否支持该控件
isSupported,
// 浏览器是否支持触屏事件
isSupportTouch: document.hasOwnProperty('ontouchstart'),
// 步骤
step: 1, // 1选择文件 2剪裁 3上传
// 上传状态及进度
loading: 0, // 0未开始 1正在 2成功 3错误
progress: 0,
// 是否有错误及错误信息
hasError: false,
errorMsg: '',
// 需求图宽高比
ratio: width / height,
// 原图地址、生成图片地址
sourceImg: null,
sourceImgUrl: '',
createImgUrl: '',
imgData: '',
// 原图片拖动事件初始值
sourceImgMouseDown: {
on: false,
mX: 0, // 鼠标按下的坐标
mY: 0,
x: 0, // scale原图坐标
y: 0,
},
// 生成图片预览的容器大小
previewContainer: {
width: 100,
height: 100,
},
// 原图容器宽高
sourceImgContainer: {
// sic
width: 240,
height: 180,
},
// 原图展示属性
scale: {
zoomAddOn: false, // 按钮缩放事件开启
zoomSubOn: false, // 按钮缩放事件开启
range: 1, //最大100
x: 0,
y: 0,
width: 0,
height: 0,
maxWidth: 0,
maxHeight: 0,
minWidth: 0, // 最宽
minHeight: 0,
naturalWidth: 0, // 原宽
naturalHeight: 0,
},
}
},
computed: {
// 进度条样式
progressStyle() {
let { progress } = this
return {
width: progress + '%',
}
},
// 原图样式
sourceImgStyle() {
let { scale, sourceImgMasking } = this,
top = scale.y + sourceImgMasking.y + 'px',
left = scale.x + sourceImgMasking.x + 'px'
return {
top,
left,
width: scale.width + 'px',
height: scale.height + 'px',
}
},
// 原图蒙版属性
sourceImgMasking() {
let { width, height, ratio, sourceImgContainer } = this,
sic = sourceImgContainer,
sicRatio = sic.width / sic.height, // 原图容器宽高比
x = 0,
y = 0,
w = sic.width,
h = sic.height,
scale = 1
if (ratio < sicRatio) {
scale = sic.height / height
w = sic.height * ratio
x = (sic.width - w) / 2
}
if (ratio > sicRatio) {
scale = sic.width / width
h = sic.width / ratio
y = (sic.height - h) / 2
}
return {
scale, // 蒙版相对需求宽高的缩放
x,
y,
width: w,
height: h,
}
},
// 原图遮罩样式
sourceImgShadeStyle() {
let { sourceImgMasking, sourceImgContainer } = this,
sic = sourceImgContainer,
sim = sourceImgMasking,
w = sim.width === sic.width ? sim.width : (sic.width - sim.width) / 2,
h = sim.height === sic.height ? sim.height : (sic.height - sim.height) / 2
return {
width: w + 'px',
height: h + 'px',
}
},
previewStyle() {
let { width, height, ratio, previewContainer } = this,
pc = previewContainer,
w = pc.width,
h = pc.height,
pcRatio = w / h
if (ratio < pcRatio) {
w = pc.height * ratio
}
if (ratio > pcRatio) {
h = pc.width / ratio
}
return {
width: w + 'px',
height: h + 'px',
}
},
},
watch: {
value(newValue) {
if (newValue && this.loading !== 1) {
this.reset()
}
},
},
methods: {
// 点击波纹效果
ripple(e) {
uploadAvatar.effectRipple(e)
},
// 关闭控件
off() {
let that = this
setTimeout(() => {
that.$emit('input', false)
if (that.step === 3 && that.loading === 2) {
that.setStep(1)
}
}, 200)
},
// 设置步骤
setStep(no) {
let that = this
setTimeout(() => {
that.step = no
}, 200)
},
/* 图片选择区域函数绑定
---------------------------------------------------------------*/
preventDefault(e) {
e.preventDefault()
return false
},
handleClick(e) {
if (this.loading !== 1) {
if (e.target !== this.$refs.fileinput) {
e.preventDefault()
if (document.activeElement !== this.$refs) {
this.$refs.fileinput.click()
}
}
}
},
handleChange(e) {
e.preventDefault()
if (this.loading !== 1) {
let files = e.target.files || e.dataTransfer.files
this.reset()
if (this.checkFile(files[0])) {
this.setSourceImg(files[0])
}
}
},
/* ---------------------------------------------------------------*/
// 检测选择的文件是否合适
checkFile(file) {
let that = this,
{ lang, maxSize } = that
// 仅限图片
if (file.type.indexOf('image') === -1) {
that.hasError = true
that.errorMsg = lang.error.onlyImg
return false
}
// 超出大小
if (file.size / 1024 > maxSize) {
that.hasError = true
that.errorMsg = lang.error.outOfSize + maxSize + 'kb'
return false
}
return true
},
// 重置控件
reset() {
this.loading = 0
this.hasError = false
this.errorMsg = ''
this.progress = 0
},
// 设置图片源
setSourceImg(file) {
let that = this,
fr = new FileReader()
fr.onload = (e) => {
that.sourceImgUrl = fr.result
that.startCrop()
}
fr.readAsDataURL(file)
},
// 剪裁前准备工作
startCrop() {
let that = this,
{ width, height, ratio, scale, sourceImgUrl, sourceImgMasking, lang } = that,
sim = sourceImgMasking,
img = new Image()
img.src = sourceImgUrl
img.onload = () => {
let nWidth = img.naturalWidth,
nHeight = img.naturalHeight,
nRatio = nWidth / nHeight,
w = sim.width,
h = sim.height,
x = 0,
y = 0
// 图片像素不达标
if (nWidth < width || nHeight < height) {
that.hasError = true
that.errorMsg = lang.error.lowestPx + width + '*' + height
return false
}
if (ratio > nRatio) {
h = w / nRatio
y = (sim.height - h) / 2
}
if (ratio < nRatio) {
w = h * nRatio
x = (sim.width - w) / 2
}
scale.range = 0
scale.x = x
scale.y = y
scale.width = w
scale.height = h
scale.minWidth = w
scale.minHeight = h
scale.maxWidth = nWidth * sim.scale
scale.maxHeight = nHeight * sim.scale
scale.naturalWidth = nWidth
scale.naturalHeight = nHeight
that.sourceImg = img
that.createImg()
that.setStep(2)
}
},
// 鼠标按下图片准备移动
imgStartMove(e) {
e.preventDefault()
// 支持触摸事件,则鼠标事件无效
if (this.isSupportTouch && !e.targetTouches) {
return false
}
let et = e.targetTouches ? e.targetTouches[0] : e,
{ sourceImgMouseDown, scale } = this,
simd = sourceImgMouseDown
simd.mX = et.screenX
simd.mY = et.screenY
simd.x = scale.x
simd.y = scale.y
simd.on = true
},
// 鼠标按下状态下移动,图片移动
imgMove(e) {
e.preventDefault()
// 支持触摸事件,则鼠标事件无效
if (this.isSupportTouch && !e.targetTouches) {
return false
}
let et = e.targetTouches ? e.targetTouches[0] : e,
{
sourceImgMouseDown: { on, mX, mY, x, y },
scale,
sourceImgMasking,
} = this,
sim = sourceImgMasking,
nX = et.screenX,
nY = et.screenY,
dX = nX - mX,
dY = nY - mY,
rX = x + dX,
rY = y + dY
if (!on) return
if (rX > 0) {
rX = 0
}
if (rY > 0) {
rY = 0
}
if (rX < sim.width - scale.width) {
rX = sim.width - scale.width
}
if (rY < sim.height - scale.height) {
rY = sim.height - scale.height
}
scale.x = rX
scale.y = rY
},
// 按钮按下开始放大
startZoomAdd(e) {
let that = this,
{ scale } = that
scale.zoomAddOn = true
function zoom() {
if (scale.zoomAddOn) {
let range = scale.range >= 100 ? 100 : ++scale.range
that.zoomImg(range)
setTimeout(() => {
zoom()
}, 60)
}
}
zoom()
},
// 按钮松开或移开取消放大
endZoomAdd(e) {
this.scale.zoomAddOn = false
},
// 按钮按下开始缩小
startZoomSub(e) {
let that = this,
{ scale } = that
scale.zoomSubOn = true
function zoom() {
if (scale.zoomSubOn) {
let range = scale.range <= 0 ? 0 : --scale.range
that.zoomImg(range)
setTimeout(() => {
zoom()
}, 60)
}
}
zoom()
},
// 按钮松开或移开取消缩小
endZoomSub(e) {
let { scale } = this
scale.zoomSubOn = false
},
zoomChange(e) {
this.zoomImg(e.target.value)
},
// 缩放原图
zoomImg(newRange) {
let that = this,
{ sourceImgMasking, sourceImgMouseDown, scale } = this,
{ maxWidth, maxHeight, minWidth, minHeight, width, height, x, y, range } = scale,
sim = sourceImgMasking,
// 蒙版宽高
sWidth = sim.width,
sHeight = sim.height,
// 新宽高
nWidth = minWidth + ((maxWidth - minWidth) * newRange) / 100,
nHeight = minHeight + ((maxHeight - minHeight) * newRange) / 100,
// 新坐标(根据蒙版中心点缩放)
nX = sWidth / 2 - (nWidth / width) * (sWidth / 2 - x),
nY = sHeight / 2 - (nHeight / height) * (sHeight / 2 - y)
// 判断新坐标是否超过蒙版限制
if (nX > 0) {
nX = 0
}
if (nY > 0) {
nY = 0
}
if (nX < sWidth - nWidth) {
nX = sWidth - nWidth
}
if (nY < sHeight - nHeight) {
nY = sHeight - nHeight
}
// 赋值处理
scale.x = nX
scale.y = nY
scale.width = nWidth
scale.height = nHeight
scale.range = newRange
setTimeout(() => {
if (scale.range === newRange) {
that.createImg()
}
}, 300)
},
// 生成需求图片
createImg(e) {
let that = this,
{
mime,
sourceImg,
scale: { x, y, width, height },
sourceImgMasking: { scale },
} = that,
canvas = that.$refs.canvas,
ctx = canvas.getContext('2d')
if (e) {
// 取消鼠标按下移动状态
that.sourceImgMouseDown.on = false
}
ctx.clearRect(0, 0, that.width, that.height)
ctx.drawImage(sourceImg, x / scale, y / scale, width / scale, height / scale)
that.createImgUrl = canvas.toDataURL(mime)
},
prepareUpload() {
let { url, createImgUrl, field, ki } = this
this.$emit('crop-success', createImgUrl, field, ki)
if (typeof url === 'string' && url) {
this.upload()
} else {
this.off()
}
},
// 上传图片
upload() {
let that = this,
{ lang, imgFormat, mime, url, params, headers, field, ki, createImgUrl } = this
this.imgData = new FormData()
let blobData = uploadAvatar.dataURItoBlob(createImgUrl, mime)
this.imgData.append('file', blobData)
this.imgData.append('user', this.userInfo._id || '')
// 添加其他参数
// if (typeof params === 'object' && params) {
// Object.keys(params).forEach((k) => {
// this.imgData.append(k, params[k])
// })
// }
// 监听进度回调
const uploadProgress = (event) => {
if (event.lengthComputable) {
that.progress = (100 * Math.round(event.loaded)) / event.total
}
}
// 上传文件
that.reset()
that.loading = 1
that.setStep(3)
new Promise((resolve, reject) => {
let client = new XMLHttpRequest()
client.open('POST', '/api/updateAvatar', true)
client.onreadystatechange = function (result) {
if (this.status === 200 || this.status === 201) {
resolve(headers.imgname)
} else {
reject(this.status)
}
}
client.upload.addEventListener('progress', uploadProgress, false) // 监听进度
// 设置header
if (typeof headers === 'object' && headers) {
Object.keys(headers).forEach((k) => {
client.setRequestHeader(k, headers[k])
})
}
let params = {
_id: this.userInfo._id || '',
imgData: this.imgData,
}
client.send(this.imgData)
}).then(
(result) => {
console.log(result)
if (that.value) {
that.loading = 2
that.reset()
this.$message({
type: 'success',
message: `成功更新头像`,
})
that.$emit('crop-upload-success', result, field, ki)
}
},
(sts) => {
if (that.value) {
that.loading = 3
that.reset()
that.hasError = true
that.errorMsg = lang.fail
that.$emit('crop-upload-fail', sts, field, ki)
}
}
)
},
},
created() {
// 绑定按键esc隐藏此插件事件
document.addEventListener('keyup', (e) => {
if (this.value && (e.key === 'Escape' || e.keyCode === 27)) {
this.off()
}
})
},
}
</script>
<style lang="scss">
@import '../assets/scss/variables.scss';
@charset "UTF-8";
@-webkit-keyframes vicp_progress {
0% {
background-position-y: 0;
}
100% {
background-position-y: 40px;
}
}
@keyframes vicp_progress {
0% {
background-position-y: 0;
}
100% {
background-position-y: 40px;
}
}
@-webkit-keyframes vicp {
0% {
opacity: 0;
-webkit-transform: scale(0) translatey(-60px);
transform: scale(0) translatey(-60px);
}
100% {
opacity: 1;
-webkit-transform: scale(1) translatey(0);
transform: scale(1) translatey(0);
}
}
@keyframes vicp {
0% {
opacity: 0;
-webkit-transform: scale(0) translatey(-60px);
transform: scale(0) translatey(-60px);
}
100% {
opacity: 1;
-webkit-transform: scale(1) translatey(0);
transform: scale(1) translatey(0);
}
}
.vue-image-crop-upload {
position: fixed;
display: block;
-webkit-box-sizing: border-box;
box-sizing: border-box;
z-index: $zindex-upload-avatar;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.65);
-webkit-tap-highlight-color: transparent;
-moz-tap-highlight-color: transparent;
}
.vue-image-crop-upload .vicp-wrap {
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
position: fixed;
display: block;
-webkit-box-sizing: border-box;
box-sizing: border-box;
z-index: $zindex-upload-avatar;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
width: 600px;
height: 330px;
padding: 25px;
background-color: #fff;
border-radius: 2px;
-webkit-animation: vicp 0.12s ease-in;
animation: vicp 0.12s ease-in;
}
.vue-image-crop-upload .vicp-wrap .vicp-close {
position: absolute;
right: -30px;
top: -30px;
}
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4 {
position: relative;
display: block;
width: 30px;
height: 30px;
cursor: pointer;
-webkit-transition: -webkit-transform 0.18s;
transition: -webkit-transform 0.18s;
transition: transform 0.18s;
transition: transform 0.18s, -webkit-transform 0.18s;
-webkit-transform: rotate(0);
-ms-transform: rotate(0);
transform: rotate(0);
}
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after,
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::before {
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
content: '';
position: absolute;
top: 12px;
left: 4px;
width: 20px;
height: 3px;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
background-color: #fff;
}
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4::after {
-webkit-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.vue-image-crop-upload .vicp-wrap .vicp-close .vicp-icon4:hover {
-webkit-transform: rotate(90deg);
-ms-transform: rotate(90deg);
transform: rotate(90deg);
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area {
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 35px;
height: 170px;
background-color: rgba(0, 0, 0, 0.03);
text-align: center;
border: 1px dashed rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 {
display: block;
margin: 0 auto 6px;
width: 42px;
height: 42px;
overflow: hidden;
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-arrow {
display: block;
margin: 0 auto;
width: 0;
height: 0;
border-bottom: 14.7px solid rgba(0, 0, 0, 0.3);
border-left: 14.7px solid transparent;
border-right: 14.7px solid transparent;
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-body {
display: block;
width: 12.6px;
height: 14.7px;
margin: 0 auto;
background-color: rgba(0, 0, 0, 0.3);
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-icon1 .vicp-icon1-bottom {
-webkit-box-sizing: border-box;
box-sizing: border-box;
display: block;
height: 12.6px;
border: 6px solid rgba(0, 0, 0, 0.3);
border-top: none;
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-hint {
display: block;
padding: 15px;
font-size: 14px;
color: #666;
line-height: 30px;
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area .vicp-no-supported-hint {
display: block;
position: absolute;
top: 0;
left: 0;
padding: 30px;
width: 100%;
height: 60px;
line-height: 30px;
background-color: #eee;
text-align: center;
color: #666;
font-size: 14px;
}
.vue-image-crop-upload .vicp-wrap .vicp-step1 .vicp-drop-area:hover {
cursor: pointer;
border-color: rgba(0, 0, 0, 0.1);
background-color: rgba(0, 0, 0, 0.05);
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop {
overflow: hidden;
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left {
float: left;
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-img-container {
position: relative;
display: block;
width: 240px;
height: 180px;
background-color: #e5e5e0;
overflow: hidden;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-img-container
.vicp-img {
position: absolute;
display: block;
cursor: move;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-img-container
.vicp-img-shade {
-webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
position: absolute;
background-color: rgba(241, 242, 243, 0.8);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-img-container
.vicp-img-shade.vicp-img-shade-1 {
top: 0;
left: 0;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-img-container
.vicp-img-shade.vicp-img-shade-2 {
bottom: 0;
right: 0;
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range {
position: relative;
margin: 30px 0;
width: 240px;
height: 18px;
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5,
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6 {
position: absolute;
top: 0;
width: 18px;
height: 18px;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.08);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
.vicp-icon5:hover,
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
.vicp-icon6:hover {
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
cursor: pointer;
background-color: rgba(0, 0, 0, 0.14);
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon5 {
left: 0;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
.vicp-icon5::before {
position: absolute;
content: '';
display: block;
left: 3px;
top: 8px;
width: 12px;
height: 2px;
background-color: #fff;
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-left .vicp-range .vicp-icon6 {
right: 0;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
.vicp-icon6::before {
position: absolute;
content: '';
display: block;
left: 3px;
top: 8px;
width: 12px;
height: 2px;
background-color: #fff;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
.vicp-icon6::after {
position: absolute;
content: '';
display: block;
top: 3px;
left: 8px;
width: 2px;
height: 12px;
background-color: #fff;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range'] {
display: block;
padding-top: 5px;
margin: 0 auto;
width: 180px;
height: 8px;
vertical-align: top;
background: transparent;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: pointer;
/* 滑块
---------------------------------------------------------------*/
/* 轨道
---------------------------------------------------------------*/
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:focus {
outline: none;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-webkit-slider-thumb {
-webkit-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
-webkit-appearance: none;
appearance: none;
margin-top: -3px;
width: 12px;
height: 12px;
background-color: #61c091;
border-radius: 100%;
border: none;
-webkit-transition: 0.2s;
transition: 0.2s;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-moz-range-thumb {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
-moz-appearance: none;
appearance: none;
width: 12px;
height: 12px;
background-color: #61c091;
border-radius: 100%;
border: none;
-webkit-transition: 0.2s;
transition: 0.2s;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-ms-thumb {
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.18);
appearance: none;
width: 12px;
height: 12px;
background-color: #61c091;
border: none;
border-radius: 100%;
-webkit-transition: 0.2s;
transition: 0.2s;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:active::-moz-range-thumb {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
width: 14px;
height: 14px;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:active::-ms-thumb {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
width: 14px;
height: 14px;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:active::-webkit-slider-thumb {
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.23);
margin-top: -4px;
width: 14px;
height: 14px;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-webkit-slider-runnable-track {
-webkit-box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
width: 100%;
height: 6px;
cursor: pointer;
border-radius: 2px;
border: none;
background-color: rgba(68, 170, 119, 0.3);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-moz-range-track {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
width: 100%;
height: 6px;
cursor: pointer;
border-radius: 2px;
border: none;
background-color: rgba(68, 170, 119, 0.3);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-ms-track {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12);
width: 100%;
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
height: 6px;
border-radius: 2px;
border: none;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-ms-fill-lower {
background-color: rgba(68, 170, 119, 0.3);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']::-ms-fill-upper {
background-color: rgba(68, 170, 119, 0.15);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:focus::-webkit-slider-runnable-track {
background-color: rgba(68, 170, 119, 0.5);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:focus::-moz-range-track {
background-color: rgba(68, 170, 119, 0.5);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:focus::-ms-fill-lower {
background-color: rgba(68, 170, 119, 0.45);
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-left
.vicp-range
input[type='range']:focus::-ms-fill-upper {
background-color: rgba(68, 170, 119, 0.25);
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right {
float: right;
}
.vue-image-crop-upload .vicp-wrap .vicp-step2 .vicp-crop .vicp-crop-right .vicp-preview {
height: 150px;
overflow: hidden;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-right
.vicp-preview
.vicp-preview-item {
position: relative;
padding: 5px;
width: 100px;
height: 100px;
float: left;
margin-right: 16px;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-right
.vicp-preview
.vicp-preview-item
span {
position: absolute;
bottom: -30px;
width: 100%;
font-size: 14px;
color: #bbb;
display: block;
text-align: center;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-right
.vicp-preview
.vicp-preview-item
img {
position: absolute;
display: block;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
padding: 3px;
background-color: #fff;
border: 1px solid rgba(0, 0, 0, 0.15);
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-right
.vicp-preview
.vicp-preview-item:nth-child(2) {
margin-right: 0;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step2
.vicp-crop
.vicp-crop-right
.vicp-preview
.vicp-preview-item:nth-child(2)
img {
border-radius: 100%;
}
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload {
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
padding: 35px;
height: 170px;
background-color: rgba(0, 0, 0, 0.03);
text-align: center;
border: 1px dashed #ddd;
}
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-loading {
display: block;
padding: 15px;
font-size: 16px;
color: #999;
line-height: 30px;
}
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap {
margin-top: 12px;
background-color: rgba(0, 0, 0, 0.08);
border-radius: 3px;
}
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-progress-wrap .vicp-progress {
position: relative;
display: block;
height: 5px;
border-radius: 3px;
background-color: #4a7;
-webkit-box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
box-shadow: 0 2px 6px 0 rgba(68, 170, 119, 0.3);
-webkit-transition: width 0.15s linear;
transition: width 0.15s linear;
background-image: -webkit-linear-gradient(
135deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
background-image: linear-gradient(
-45deg,
rgba(255, 255, 255, 0.2) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.2) 75%,
transparent 75%,
transparent
);
background-size: 40px 40px;
-webkit-animation: vicp_progress 0.5s linear infinite;
animation: vicp_progress 0.5s linear infinite;
}
.vue-image-crop-upload
.vicp-wrap
.vicp-step3
.vicp-upload
.vicp-progress-wrap
.vicp-progress::after {
content: '';
position: absolute;
display: block;
top: -3px;
right: -3px;
width: 9px;
height: 9px;
border: 1px solid rgba(245, 246, 247, 0.7);
-webkit-box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
box-shadow: 0 1px 4px 0 rgba(68, 170, 119, 0.7);
border-radius: 100%;
background-color: #4a7;
}
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-error,
.vue-image-crop-upload .vicp-wrap .vicp-step3 .vicp-upload .vicp-success {
height: 100px;
line-height: 100px;
}
.vue-image-crop-upload .vicp-wrap .vicp-operate {
position: absolute;
right: 20px;
bottom: 20px;
}
.vue-image-crop-upload .vicp-wrap .vicp-operate a {
position: relative;
float: left;
display: block;
margin-left: 10px;
width: 100px;
height: 36px;
line-height: 36px;
text-align: center;
cursor: pointer;
font-size: 14px;
color: #4a7;
border-radius: 2px;
overflow: hidden;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.vue-image-crop-upload .vicp-wrap .vicp-operate a:hover {
background-color: rgba(0, 0, 0, 0.03);
}
.vue-image-crop-upload .vicp-wrap .vicp-error,
.vue-image-crop-upload .vicp-wrap .vicp-success {
display: block;
font-size: 14px;
line-height: 24px;
height: 24px;
color: #d10;
text-align: center;
vertical-align: top;
}
.vue-image-crop-upload .vicp-wrap .vicp-success {
color: #4a7;
}
.vue-image-crop-upload .vicp-wrap .vicp-icon3 {
position: relative;
display: inline-block;
width: 20px;
height: 20px;
top: 4px;
}
.vue-image-crop-upload .vicp-wrap .vicp-icon3::after {
position: absolute;
top: 3px;
left: 6px;
width: 6px;
height: 10px;
border-width: 0 2px 2px 0;
border-color: #4a7;
border-style: solid;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
content: '';
}
.vue-image-crop-upload .vicp-wrap .vicp-icon2 {
position: relative;
display: inline-block;
width: 20px;
height: 20px;
top: 4px;
}
.vue-image-crop-upload .vicp-wrap .vicp-icon2::after,
.vue-image-crop-upload .vicp-wrap .vicp-icon2::before {
content: '';
position: absolute;
top: 9px;
left: 4px;
width: 13px;
height: 2px;
background-color: #d10;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.vue-image-crop-upload .vicp-wrap .vicp-icon2::after {
-webkit-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.e-ripple {
position: absolute;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.15);
background-clip: padding-box;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-transform: scale(0);
-ms-transform: scale(0);
transform: scale(0);
opacity: 1;
}
.e-ripple.z-active {
opacity: 0;
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
}
@media screen and (max-width: $mobile-screen) {
.main .vue-image-crop-upload {
.vicp-wrap {
position: absolute;
width: 100%;
height: 90%;
max-height: 500px;
.vicp-crop {
.vicp-crop-left {
float: none;
.vicp-img-container {
height: 180px;
margin: auto;
}
}
.vicp-crop-right {
float: none;
.vicp-preview {
margin: auto;
width: 100%;
height: 130px;
.vicp-preview-item {
display: inline-block;
float: none;
margin: auto;
}
}
}
}
}
}
}
</style>
================================================
FILE: src/components/Waline.vue
================================================
<template>
<div class="nice-comment">
<h2 class="page-second-title">
<strong>精彩绝伦的评论</strong>
</h2>
<div id="waline"></div>
</div>
</template>
<script>
require('@waline/client/dist/waline')
import '@waline/client/dist/waline.css'
export default {
name: 'Waline',
mounted() {
// Add Waline Comment Functions @2022.01.24
this.$nextTick(() => {
Waline.init({
el: '#waline',
serverURL: 'https://comment.nicelinks.site/',
copyright: false,
wordLimit: [10, 600],
requiredMeta: ['nick', 'mail'],
})
})
},
}
</script>
<style lang="scss">
.nice-comment {
padding: 0 20px;
}
#waline {
width: 100%;
min-width: 30rem;
padding: 15px 0;
aspect-ratio: 3 / 2;
.wl-panel {
font-size: 1.4rem;
.wl-header {
label,
input {
font-size: 1.4rem;
}
}
.wl-editor {
font-size: 1.6rem;
}
.wl-actions {
flex: 1;
}
.wl-preview {
h4 {
font-size: 1.6rem;
}
}
}
.wl-content {
font-size: 1.6rem;
h3 {
font-size: 1.8rem;
font-weight: 600;
}
p {
margin-top: 1rem;
font-size: 1.6rem;
}
ul {
margin: 1.6rem 0 1.6rem 3.2rem;
li {
list-style: disc;
}
}
ol {
margin: 1.6rem 0 1.6rem 3.2rem;
li {
list-style: decimal;
}
}
}
.wl-btn,
.wl-text-number,
.wl-nick,
.wl-time {
font-size: 1.4rem;
}
.wl-sort li {
font-size: 1.4rem;
}
.wl-count,
.wl-empty {
font-size: 1.6rem;
}
.wl-action:nth-child(1),
.wl-action:nth-child(3) {
display: none;
}
.vbtn {
display: none;
}
.primary {
display: block;
}
.vcards {
ul {
padding-left: 1rem;
li {
list-style: circle;
}
}
}
.v {
padding: 0 20px;
}
.v[data-class='v'] {
blockquote {
border-left-width: 4px;
p {
color: #9393aa;
}
}
.vcomment {
display: block;
}
.vpanel {
margin: 0.5em auto;
}
}
}
</style>
================================================
FILE: src/components/dialog/AdBlockDialog.vue
================================================
<template>
<div class="block-tip-dialog">
<div class="dlg-header">
<h2 class="title">
若此弹框悬浮出来,多是由 AdBlock 触发
</h2>
<p class="warm-reminder">您可将本站加入白名单,解除广告屏蔽(ABP),感谢支持</p>
<button type="button" class="btn-close" @click="onCloseClick">
<span class="icon-cross"></span>
</button>
</div>
<div class="pannel">
<div class="item">
<img class="qrcode" src="/static/img/reward_wexin.jpg" alt="微信打赏" />
<strong class="text font-medium">“月黑见渔灯,</strong>
<span class="text font-medium">微信打赏</span>
</div>
<div class="item">
<img class="qrcode" src="/static/img/reward_zhifubao.jpg" alt="倾城之链-小程序" />
<strong class="text font-medium">“孤光一点萤。”</strong>
<span class="text font-medium">支付宝打赏</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AdBlockDialog',
methods: {
onCloseClick() {
this.$emit('close')
},
},
}
</script>
<style lang="scss" scoped>
@import './../../assets/scss/variables.scss';
$factor: 0.5rem;
@mixin cross($size: 20px, $color: currentColor, $thickness: 1px) {
margin: 0;
padding: 0;
border: 0;
background: none;
position: relative;
width: $size;
height: $size;
&:before,
&:after {
content: '';
position: absolute;
top: ($size - $thickness) / 2;
left: 0;
right: 0;
height: $thickness;
background: $color;
border-radius: $thickness;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
span {
display: block;
}
}
.block-tip-dialog {
position: fixed;
top: 1.5 * $header-height;
left: 50%;
transform: translateX(-50%);
width: 100 * $factor;
height: 68 * $factor;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
background-color: $white;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.618);
animation: moveto 0.618s;
-webkit-animation: moveto 0.618s;
animation-timing-function: cubic-bezier(0, 0.13, 0.14, 1);
padding: 2 * $factor;
border-radius: 2 * $factor;
z-index: $zindex-auto-dialog;
.dlg-header {
width: 100%;
position: relative;
.title {
font-size: $font-large;
font-weight: 500;
}
.warm-reminder {
margin: 2 * $factor 0;
}
.btn-close {
position: absolute;
top: -10 * $factor;
right: -6 * $factor;
width: 8 * $factor;
height: 8 * $factor;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
margin: 0;
border: 0;
padding: 0;
background-color: $producthunt;
border-radius: 50%;
cursor: pointer;
transition: all 150ms;
.icon-cross {
@include cross(26px, #fff, 1px);
}
&:hover,
&:focus {
transform: rotateZ(90deg);
background-color: $red;
}
}
}
.pannel {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 100%;
height: 40 * $factor;
.item {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
width: 40 * $factor;
height: 100%;
.qrcode {
width: 32 * $factor;
height: 32 * $factor;
}
.text {
font-size: $font-small;
color: $common-link;
}
}
}
}
@keyframes moveto {
from {
opacity: 0.618;
top: -88 * $factor;
}
to {
opacity: 1;
top: 1.5 * $header-height;
}
}
</style>
================================================
FILE: src/components/dialog/AutoDialog.vue
================================================
<template>
<div class="auto-dialog">
<div class="dlg-header">
<h2 class="title">
订阅倾城
<a target="_blank" rel="noopener" @click="onOpenWeekly"
href="https://blog.nicelinks.site/?ref=nicelinks.site">每周精要
</a>
</h2>
<button type="button" class="btn-close" @click="onCloseClick">
<span class="icon-cross"></span>
</button>
</div>
<div class="pannel">
<div class="item">
<img class="qrcode" src="https://image.nicelinks.site/qrcode_jqx.jpg" alt="晚晴幽草轩-公众号" />
<span class="text font-medium">晚晴幽草轩</span>
<span class="text">微信扫码关注</span>
</div>
<div class="item">
<img class="qrcode"
src="https://image.nicelinks.site/nicelinks-miniprogram-code.jpeg?imageView2/1/w/250/h/250/interlace/1/ignore-error/1"
alt="倾城之链-小程序" />
<span class="text font-medium">倾城之链</span>
<span class="text">微信扫码体验</span>
</div>
</div>
</div>
</template>
<script>
import { AUTO_DIALOG } from 'config/constant'
import { setLocalStorage } from './../../helper/tool'
export default {
name: 'AutoDialog',
methods: {
onCloseClick() {
setLocalStorage(AUTO_DIALOG, true)
this.$emit('close')
},
onOpenWeekly() {
this.$gtagTracking('open-weekly', 'global')
}
},
}
</script>
<style lang="scss" scoped>
@import './../../assets/scss/variables.scss';
$factor: 0.5rem;
@mixin cross($size: 20px, $color: currentColor, $thickness: 1px) {
margin: 0;
padding: 0;
border: 0;
background: none;
position: relative;
width: $size;
height: $size;
&:before,
&:after {
content: '';
position: absolute;
top: ($size - $thickness) / 2;
left: 0;
right: 0;
height: $thickness;
background: $color;
border-radius: $thickness;
}
&:before {
transform: rotate(45deg);
}
&:after {
transform: rotate(-45deg);
}
span {
display: block;
}
}
.auto-dialog {
position: fixed;
bottom: 32 * $factor;
right: 18 * $factor;
width: 90 * $factor;
height: 60 * $factor;
display: flex;
flex-direction: column;
justify-content: space-evenly;
align-items: center;
background-color: $white;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.618);
animation: automoveto 0.618s;
-webkit-animation: automoveto 0.618s;
animation-timing-function: cubic-bezier(0, 0.13, 0.14, 1);
padding: 2 * $factor;
border-radius: 2 * $factor;
z-index: $zindex-auto-dialog;
.dlg-header {
width: 100%;
position: relative;
.title {
font-size: $font-large;
font-weight: 500;
}
.btn-close {
position: absolute;
top: -10 * $factor;
right: -6 * $factor;
width: 8 * $factor;
height: 8 * $factor;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
margin: 0;
border: 0;
padding: 0;
background-color: $producthunt;
border-radius: 50%;
cursor: pointer;
transition: all 150ms;
.icon-cross {
@include cross(26px, #fff, 1px);
}
&:hover,
&:focus {
transform: rotateZ(90deg);
background-color: $red;
}
}
}
.pannel {
display: flex;
flex-direction: row;
justify-content: space-around;
align-items: center;
width: 100%;
height: 40 * $factor;
.item {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 40 * $factor;
height: 100%;
.qrcode {
width: 32 * $factor;
height: 32 * $factor;
}
.text {
font-size: $font-small;
color: $common-link;
}
}
}
}
@keyframes automoveto {
from {
opacity: 0.618;
right: -80 * $factor;
}
to {
opacity: 1;
right: 18 * $factor;
}
}
</style>
================================================
FILE: src/components/dialog/EditDialog.vue
================================================
<template>
<div id="edit-dialog">
<el-dialog stripe :title="$t('shareNewLink')" :visible.sync="isShowDlgFlag" v-if="isShowDlgFlag" size="small"
v-loading.body="isLoading">
<div class="form form-horizontal">
<el-form :model="fillForm" :rules="rules" ref="fillForm">
<div class="form-group">
<el-form-item prop="urlPath">
<el-input v-model="fillForm.urlPath" :placeholder="this.$t('pleaseEnter') + this.$t('linkAddressStr')">
</el-input>
</el-form-item>
</div>
<div class="form-group">
<el-form-item prop="title">
<el-input v-model="fillForm.title" :placeholder="this.$t('pleaseEnter') + this.$t('linkNameStr')">
</el-input>
</el-form-item>
</div>
<div class="form-group">
<el-form-item prop="classify">
<el-select class="wrap-block" v-model="fillForm.classify"
:placeholder="this.$t('pleaseSelect') + this.$t('linkClassifyStr')">
<el-option v-for="item in classifyList" :key="item.key" :label="$t(item.name)" :value="item.value">
</el-option>
</el-select>
</el-form-item>
</div>
<div class="form-group">
<div class="col-sm-8">
<el-form-item prop="theme">
<el-select class="wrap-block" v-model="fillForm.theme"
:placeholder="this.$t('pleaseSelect') + this.$t('linkThemeStr')">
<el-option v-for="item in themeList" :key="item.key" :label="item.key" :value="item.value">
</el-option>
</el-select>
</el-form-item>
</div>
</div>
<div class="form-group">
<el-form-item prop="tags">
<el-select class="wrap-block" v-model="fillForm.tags" allow-create multiple filterable :multiple-limit="3"
:placeholder="this.$t('pleaseSelect') + this.$t('linkTagsStr')">
<el-option v-for="item in tagsList" :key="item" :label="item" :value="item">
</el-option>
</el-select>
</el-form-item>
</div>
<div class="form-group">
<el-form-item prop="keywords">
<el-input type="textarea" :maxlength="360" :autosize="{ minRows: 2, maxRows: 10 }"
:placeholder="this.$t('pleaseSelect') + this.$t('linkKeywordStr')" v-model="fillForm.keywords">
</el-input>
</el-form-item>
</div>
<div class="form-group">
<el-form-item prop="desc">
<el-input type="textarea" :maxlength="360" :autosize="{ minRows: 3, maxRows: 10 }"
:placeholder="this.$t('pleaseSelect') + this.$t('linkDescStr')" v-model="fillForm.desc">
</el-input>
</el-form-item>
</div>
<div class="form-group">
<markdown v-model="fillForm.review" :placeholder="this.$t('pleaseSelect') + this.$t('linkReviewStr')" />
</div>
<div class="form-group">
<label class="control-label"> {{ this.$t('isAcive') }} :</label>
<el-switch :on-text="$t('yes')" :off-text="$t('no')" v-model="fillForm.active" on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</div>
<div class="form-group">
<label class="control-label"> {{ this.$t('isAlive') }} :</label>
<el-switch :on-text="$t('yes')" :off-text="$t('no')" v-model="fillForm.alive" on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</div>
<div class="form-group">
<label class="control-label"> {{ this.$t('isRecommend') }} :</label>
<el-switch :on-text="$t('yes')" :off-text="$t('no')" v-model="fillForm.recommend" on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</div>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="isShowDlgFlag = false">{{ this.$t('cancel') }}</el-button>
<el-button type="primary" @click="onCommitClick">{{ this.$t('confirm') }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import CLASSIFY_CONF from './../../config/classify'
import THEME_CONF from './../../config/theme'
import TAG_CONF from './../../config/tags'
import Markdown from 'components/markdown/Index'
export default {
name: 'EditDialog',
data() {
return {
isShowDlgFlag: false,
isLoading: false,
fillForm: {
urlPath: '',
title: '',
desc: '',
classify: '',
theme: '',
tags: [],
review: '',
alive: true,
active: false,
recommend: false
},
themeList: [],
tagsList: TAG_CONF,
classifyList: CLASSIFY_CONF,
rules: {
urlPath: [{ required: true, validator: this.$verifyUrl, trigger: 'change,blur' }],
title: [
{
required: true,
message: this.$t('pleaseEnter') + this.$t('linkNameStr'),
trigger: 'change,blur',
},
],
theme: [
{
required: true,
message: this.$t('pleaseSelect') + this.$t('linkThemeStr'),
trigger: 'change,blur',
},
],
classify: [
{
required: true,
message: this.$t('pleaseSelect') + this.$t('linkClassifyStr'),
trigger: 'change,blur',
},
],
},
}
},
components: {
Markdown,
},
props: {
value: {
type: Boolean,
default: false,
},
pdata: {
type: Object,
default: {},
},
},
watch: {
value(val) {
this.isShowDlgFlag = val
},
isShowDlgFlag(val) {
this.$emit('input', val)
},
'fillForm.classify': function (val) {
this.themeList = THEME_CONF[this.fillForm.classify] || []
},
pdata(val) {
this.fillForm = this.$cloneDeep(val)
},
},
methods: {
onCommitClick() {
this.$refs.fillForm.validate((valid) => {
if (valid) {
this.isLoading = true
let params = this.$cloneDeep(this.fillForm, true)
// 其基本信息不能改变,只是加上管理者 Id & Role @17-10-02;
params.managerId = this.userInfo && this.userInfo._id
params.managerRole = this.userInfo && this.userInfo.role
this.$apis
.updateNiceLinks(params)
.then((result) => {
this.isLoading = false
this.isShowDlgFlag = false
this.$message({
message: '干的漂亮,您已成功修改该链接',
type: 'success',
})
this.$emit('update-success')
})
.catch((error) => {
console.log(error)
this.isLoading = false
this.$message.error(`${error}`)
})
}
})
},
}
}
</script>
================================================
FILE: src/components/dialog/SentencesDialog.vue
================================================
<template>
<div class="sentences-dialog">
<el-dialog stripe :title="$t('shareNewSentences')" :visible.sync="isShowDlgFlag" v-if="isShowDlgFlag" size="small"
v-loading.body="isLoading">
<div class="form form-horizontal">
<el-form :model="fillForm" :rules="rules" ref="fillForm">
<div class="form-group">
<label class="col-sm-3 control-label"> {{ this.$t('type') }} <em>*</em>:</label>
<div class="col-sm-8">
<el-form-item prop="type">
<el-select class="wrap-block" v-model="fillForm.type"
:placeholder="this.$t('pleaseSelect') + this.$t('type')">
<el-option v-for="item in sentencesTypeList" :key="item.value" :label="item.text" :value="item.value">
</el-option>
</el-select>
</el-form-item>
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label"> {{ this.$t('content') }} :</label>
<div class="col-sm-8">
<markdown v-model="fillForm.content" :placeholder="this.$t('pleaseSelect') + this.$t('content')" />
</div>
</div>
<div class="form-group">
<label class="control-label"> {{ this.$t('isAcive') }} :</label>
<el-switch :on-text="$t('yes')" :off-text="$t('no')" v-model="fillForm.active" on-color="#13ce66"
off-color="#ff4949">
</el-switch>
</div>
</el-form>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="isShowDlgFlag = false">{{ this.$t('cancel') }}</el-button>
<el-button type="primary" @click="onCommitClick">{{ this.$t('confirm') }}</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import SENTENCES_CONF from './../../config/sentences'
import Markdown from 'components/markdown/Index'
export default {
name: 'EditDialog',
data() {
return {
isShowDlgFlag: false,
isLoading: false,
sentencesTypeList: Object.freeze(SENTENCES_CONF),
fillForm: {
type: '',
content: '',
active: false,
},
rules: {
content: [
{
required: true,
message: this.$t('pleaseEnter') + this.$t('linkNameStr'),
trigger: 'change,blur',
},
],
type: [
{
required: true,
message: this.$t('pleaseSelect') + this.$t('type'),
trigger: 'change,blur',
},
],
},
}
},
components: {
Markdown,
},
props: {
value: {
type: Boolean,
default: false,
},
pdata: {
type: Object,
default: {},
},
},
watch: {
value(val) {
this.isShowDlgFlag = val
},
isShowDlgFlag(val) {
if (!val) {
this.fillForm = {
type: '',
content: '',
active: false,
}
}
this.$emit('input', val)
},
pdata(val) {
this.fillForm = this.$cloneDeep(val)
},
},
methods: {
onCommitClick() {
this.$refs.fillForm.validate((valid) => {
if (valid) {
this.isLoading = true
let params = this.$cloneDeep(this.fillForm, true)
// 其基本信息不能改变,只是加上管理者 Id & Role @17-10-02;
params.managerId = this.userInfo && this.userInfo._id
params.createdBy = this.userInfo && this.userInfo.username
params.managerRole = this.userInfo && this.userInfo.role
this.$apis
.updateSentences(params)
.then((result) => {
this.isLoading = false
this.isShowDlgFlag = false
this.$message({
message: '干的漂亮,您已成功更新该箴言',
type: 'success',
})
this.$emit('update-success')
})
.catch((error) => {
console.log(error)
this.isLoading = false
this.$message.error(`${error}`)
})
}
})
},
}
}
</script>
<style lang="scss">
@import './../../assets/scss/variables.scss';
.sentences-dialog {
.form-group {
.control-label {
font-weight: normal;
padding: 1rem 0;
em {
color: $red;
padding-right: 3px;
}
}
}
}
</style>
================================================
FILE: src/components/homepage/HomeLotus.vue
================================================
<template>
<section class="lotus fade-in animated" id="lotus">
<article class="twelve columns lotus-animations">
<div class="flower-container">
<div class="flower">
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
<span class="part"></span>
</div>
<div class="unit-wrapper">
<h1 class="headline large-font">{{ $t('niceLinksStr') }}</h1>
<section class="hero-description">
<strong class="desc-text function"
>开放型新一代导航平台,旨在云集全球优秀网站,探索互联网中更广阔的世界;</strong
>
<strong class="desc-text objective"
>在「<mark class="mark">倾城之链</mark
>」,您可以轻松发现、学习、分享更多有用或有趣的事物。</strong
>
<router-link to="/explore/all" @click.native="onExploreAll" class="explore-all-link">
{{ $t('exploreNice') }}
</router-link>
</section>
</div>
</div>
</article>
</section>
</template>
<script>
export default {
name: 'HomeLotus',
methods: {
onExploreAll() {
this.$gtagTracking('mian-explore-all', 'index')
},
},
}
</script>
<style lang="scss" scoped>
@import './../../assets/scss/variables.scss';
@import './../../assets/scss/mixins.scss';
@media (min-width: 550px) {
.twelve.columns {
width: 100%;
margin-left: 0;
}
}
.flower-container {
position: relative;
.unit-wrapper {
position: absolute;
width: 66%;
top: 0;
bottom: 25%;
left: 0;
right: 0;
margin: auto;
opacity: 0;
animation-delay: 2.75s;
animation: show-text 1s 2.5s forwards cubic-bezier(0.1, 0.95, 0.59, 1.22);
.headline {
position: absolute;
top: calc(50% - 2em);
left: -10em;
right: -10em;
text-align: center;
font-weight: 400;
text-shadow: 0 0 2em $white;
}
.hero-description {
position: absolute;
top: 50%;
left: 0;
right: 0;
font-size: 18px;
line-height: 18px;
text-align: center;
text-shadow: 0 0 2em $white;
.desc-text {
display: block;
font-weight: bold;
line-height: 2.6rem;
}
.function {
-webkit-text-fill-color: transparent;
background: -webkit-linear-gradient(50deg, #17223b, #ff6768);
-webkit-background-clip: text;
}
.objective {
margin-top: 1.8rem;
-webkit-text-fill-color: transparent;
background: -webkit-linear-gradient(220deg, #17223b, #ff6768);
-webkit-background-clip: text;
.mark {
background-color: transparent;
}
}
.explore-all-link {
display: inline-block;
border: 2px solid $producthunt;
padding: 2rem 8rem;
margin-top: 3.6rem;
color: $producthunt;
font-weight: bold;
font-size: 2.4rem;
line-height: 2.6rem;
border-radius: 4rem;
transition: all 0.2s ease;
box-sizing: border-box;
&:hover {
color: $white;
background-color: $producthunt;
}
}
}
}
}
.flower-container {
padding-bottom: 8em;
margin: 0em 0 -6em;
overflow: hidden;
}
.flower-container .flower {
position: relative;
display: block;
height: 14em;
width: 14em;
font-size: 22px;
margin: 5em auto 4.25em;
transition: all 0.2s ease-out;
}
body[theme='dark'] .flower-container {
filter: invert(1) hue-rotate(180deg);
}
.flower-container .part {
position: absolute;
top: 50%;
left: 50%;
}
.flower-container .part:nth-child(1) {
height: 10em;
width: 10em;
margin: -10em 0 0;
border-radius: 100% 0;
background: rgba(199, 212, 227, 0.5);
opacity: 0.6;
transform-origin: 0 100% 0;
background-color: rgba(199, 212, 227, 0.5) !important;
opacity: 1;
transform: rotate(0deg);
box-shadow: 0 2.75em 4.5em rgba(0, 0, 0, 0.2);
}
.flower-container .part:nth-child(2) {
height: 10em;
width: 10em;
margin: -10em 0 0;
border-radius: 100% 0;
background: rgba(199, 212, 227, 0.5);
opacity: 0.6;
transform-origin: 0 100% 0;
background-color: rgba(199, 212, 227, 0.5) !important;
opacity: 1;
transform: rotate(90deg);
box-shadow: 2.75em 0 4.5em rgba(0, 0, 0, 0.2);
}
.flower-container .part:nth-child(3) {
height: 10em;
width: 10em;
margin: -10em 0 0;
border-radius: 100% 0;
background: rgba(199, 212, 227, 0.5);
opacity: 0.6;
transform-origin: 0 100% 0;
background-color: rgba(199, 212, 227, 0.5) !important;
opacity: 1;
transform: rotate(180deg);
box-shadow: 0 -2.75em 4.5em rgba(0, 0, 0, 0.2);
}
.flower-container .part:nth-child(4) {
height: 10em;
width: 10em;
margin: -10em 0 0;
border-radius: 100% 0;
background: rgba(199, 212, 227, 0.5);
opacity: 0.6;
transform-origin: 0 100% 0;
background-color: rgba(199, 212, 227, 0.5) !important;
opacity: 1;
transform: rotate(270deg);
box-shadow: -2.75em 0 4.5em rgba(0, 0, 0, 0.2);
}
.flower-container .part:nth-child(5) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(0deg);
}
.flower-container .part:nth-child(6) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(45deg);
}
.flower-container .part:nth-child(7) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(90deg);
}
.flower-container .part:nth-child(8) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(135deg);
}
.flower-container .part:nth-child(9) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(180deg);
}
.flower-container .part:nth-child(10) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(225deg);
}
.flower-container .part:nth-child(11) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(270deg);
}
.flower-container .part:nth-child(12) {
background: rgba(121, 103, 158, 0.5);
position: absolute;
top: 50%;
left: 50%;
display: block;
height: 6em;
width: 6em;
opacity: 0.7;
border-radius: 100% 0;
margin-top: -6em;
margin-left: 0em;
transform-origin: 0 100% 0;
background-color: rgba(121, 103, 158, 0.5) !important;
transform: rotate(315deg);
}
.lotus-animations .flower-container .part {
opacity: 0;
animation: linear forwards 2s;
}
.lotus-animations .part:nth-child(1) {
animation-name: show-large-leaf;
animation-delay: 0.5s;
}
.lotus-animations .part:nth-child(2) {
animation-name: show-large-leaf;
animation-delay: 1s;
}
.lotus-animations .part:nth-child(3) {
animation-name: show-large-leaf;
animation-delay: 1.5s;
}
.lotus-animations .part:nth-child(4) {
animation-name: show-large-leaf;
animation-delay: 2s;
}
.lotus-animations .part:nth-child(5) {
animation-name: show-small-leaf;
animation-delay: 0.25s;
}
.lotus-animations .part:nth-child(6) {
animation-name: show-small-leaf;
animation-delay: 0.5s;
}
.lotus-animations .part:nth-child(7) {
animation-name: show-small-leaf;
animation-delay: 0.75s;
}
.lotus-animations .part:nth-child(8) {
animation-name: show-small-leaf;
animation-delay: 1s;
}
.lotus-animations .part:nth-child(9) {
animation-name: show-small-leaf;
animation-delay: 1.25s;
}
.lotus-animations .part:nth-child(10) {
animation-name: show-small-leaf;
animation-delay: 1.5s;
}
.lotus-animations .part:nth-child(11) {
animation-name: show-small-leaf;
animation-delay: 1.75s;
}
.lotus-animations .part:nth-child(12) {
animation-name: show-small-leaf;
animation-delay: 2s;
}
@keyframes show-large-leaf {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes show-small-leaf {
0% {
opacity: 0;
}
100% {
opacity: 0.7;
}
}
@keyframes show-text {
0% {
opacity: 0;
transform: translateY(1em);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: $mobile-screen) {
.hero-description {
br {
display: none;
}
}
}
@media (max-width: 768px) {
.flower-container {
.flower {
font-size: 1.8rem !important;
}
.unit-wrapper {
width: 100%;
.hero-description {
font-size: 2.2rem;
padding: 0px 20px;
line-height: 2.6rem;
.explore-all-link {
padding: 1rem 4rem;
margin: 1.8rem auto;
line-height: 2.6rem;
}
}
}
}
}
@media (max-width: 560px) {
.flower-container {
.unit-wrapper .hero-description {
font-size: 2rem;
padding: 0px 10px;
}
}
}
@media (max-width: 414px) {
.flower-container {
.flower {
font-size: 1.8rem !important;
}
.unit-wrapper .hero-description {
font-size: 1.8rem;
padding: 0px 10px;
}
}
}
@media (max-width: 375px) {
.flower-container .flower {
font-size: 1.6rem !important;
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.fade-in {
animation-name: fade-in;
}
.animated {
animation-duration: 1s;
animation-fill-mode: both;
visibility: visible;
animation-name: fade-in;
}
.animated.infinite {
animation-iteration-count: infinite;
}
.animated.hinge {
animation-duration: 6s;
}
</style>
================================================
FILE: src/components/homepage/Introduction.vue
================================================
<template>
<section>
<article class="introduction">
<h2 class="find-title medium-font">探索更广阔的世界,为您</h2>
<p class="desc" v-html="forFindMoreDesc"></p>
</article>
<article class="introduction share-more">
<h2 class="share-title medium-font">分享,为您所欢喜的网站</h2>
<p class="desc" v-html="forShareMoreDesc"></p>
</article>
</section>
</template>
<script>
export default {
name: 'Introduction',
data() {
return {
forFindMoreDesc: `在这个信息化的世界,海量的讯息可能让您不知所措;担心错过她而努力汲取的同时,却可能错过更多;<br><a href="/explore/all"><strong>「倾城之链」</strong></a>的存在,即是为您解决这种困扰;在这里,您可以浏览全球各类智慧的结晶;<br>丰富视野的同时,可以标注抑或分享您喜欢的站点,从而为更多挖掘讯息的人提供建设性参考。`,
forShareMoreDesc: `<mark>在当今这信息化时代,即便是再小的个体,也当有自己的品牌。</mark>然而,独立的才是自己的。<br><a href="/explore/all">「倾城之链」</a>作为一个开放平台,鼓励您创造属于您的个人品牌,烙印着自己的风格,替自己代言、发声。<br>如果您已经这样做了,您可以尽情分享在这里,让更多人知道,且从中受益。<br>当然,您也可以分享出您欢喜的其他有意思站点,让您的见识惠及更多人。`,
}
}
}
</script>
<style lang="scss">
@import './../../assets/scss/variables.scss';
@import './../../assets/scss/mixins.scss';
.introduction {
width: 100%;
height: $section-height;
padding: 0 15%;
@include flex-box-center(column);
color: $white;
background-color: $black;
.find-title {
-webkit-text-fill-color: transparent;
background: -webkit-linear-gradient(-70deg, #ff7170, #ffe57f);
-webkit-background-clip: text;
}
.desc {
margin-top: 1.8rem;
text-align: center;
font-size: $font-medium;
line-height: 2.6rem;
}
}
.share-more {
color: $black;
background-color: $white;
.share-title {
-webkit-text-fill-color: transparent;
background: -webkit-linear-gradient(-70deg, #9867f0, #ed4e50);
-webkit-background-clip: text;
}
mark {
background-color: $white;
}
}
@media (max-width: 768px) {
.introduction {
padding: 0 1.8rem;
.desc {
text-align: left;
font-size: 1.8rem;
line-height: 2rem;
br {
content: '';
display: block;
margin: 1rem 0;
}
}
}
}
@media (max-width: 375px) {
.introduction {
padding: 0 1.6rem;
.desc {
font-size: 1.6rem;
}
}
}
</style>
================================================
FILE: src/components/homepage/LinkCountup.vue
================================================
<template>
<div class="countup-area">
<div class="top-crescent" v-if="!$isMobile"></div>
<h2 class="countup-title medium-font">已经收录优质网站个数</h2>
<CountUp
id="countup-number"
class="countup-number"
:start="0"
:end="theDisplayCount"
:decimals="0"
:duration="2.5"
:options="countUpoptions"
@callback="onCountUpCallback"
>
</CountUp>
</div>
</template>
<script>
import CountUp from 'components/CountUp'
import { isElementInViewport } from './../../helper/tool'
export default {
name: 'LinkCountup',
data() {
return {
theDisplayCount: 0,
totalLinksCount: 0,
countUpoptions: {
useEasing: true,
useGrouping: true,
separator: ',',
decimal: '.',
prefix: '',
suffix: '',
},
handleScrollEvent: null
}
},
components: {
CountUp,
},
created() {
let params = { active: true }
this.$apis
.getAllLinksCount(params)
.then((result) => {
this.totalLinksCount = result
this.handleDisplayCount()
})
.catch((error) => {
this.totalLinksCount = 99
this.handleDisplayCount()
console.log(error)
})
},
methods: {
handleDisplayCount() {
const countupNumberNode = document.getElementById('countup-number')
this.handleScrollEvent = (element) => {
const isInViewport = isElementInViewport(countupNumberNode)
if (isInViewport) {
this.theDisplayCount = this.totalLinksCount
}
}
window.addEventListener('scroll', this.handleScrollEvent)
},
/* -----------------------on***Event----------------------- */
onCountUpCallback: () => {},
},
destroyed() {
window.removeEventListener('scroll', this.handleScrollEvent)
}
}
</script>
<style lang="scss">
@import './../../assets/scss/variables.scss';
@import './../../assets/scss/mixins.scss';
.countup-area {
position: relative;
width: 100%;
height: $section-height;
@include flex-box-center(column);
color: $black;
background: $white-grey;
background: -webkit-linear-gradient(to top, $white-grey, #eef2f3);
background: linear-gradient(to top, $white-grey, #eef2f3);
.top-crescent {
position: absolute;
top: -$crescent-height / 2;
width: 100%;
height: $crescent-height;
clip-path: ellipse(64% 50% at 50% 50%);
background-color: $white;
}
.countup-title {
-webkit-text-fill-color: transparent;
background: -webkit-linear-gradient(-70deg, #9867f0, #ed4e50);
-webkit-background-clip: text;
}
.countup-number {
display: block;
margin-top: 10px;
}
}
</style>
================================================
FILE: src/components/homepage/NiceFantasy.vue
================================================
<template>
<section class="nice-fantasy">
<img class="hero" :src="niceImageSrc" :alt="$t('niceLinksStr')" />
</section>
</template>
<script>
import { getRandomInt, specifiedPadding } from './../../helper/tool'
export default {
name: 'NiceFantasy',
computed: {
niceImageSrc() {
const primaryPath = `//image.nicelinks.site/jpg/nice-links-@#.jpg`
const randomNum = getRandomInt(0,
gitextract_dss6p7fj/
├── .babelrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .npmrc
├── .prettierignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── ads.txt
├── build/
│ ├── build.js
│ ├── check-versions.js
│ ├── deploy.js
│ ├── dev-client.js
│ ├── dev-server.js
│ ├── generate-sitemap.js
│ ├── load-minified.js
│ ├── service-worker-dev.js
│ ├── service-worker-prod.js
│ ├── utils.js
│ ├── vendor-manifest.json
│ ├── vue-loader.conf.js
│ ├── webpack.base.conf.js
│ ├── webpack.dev.conf.js
│ ├── webpack.dll.conf.js
│ ├── webpack.prod.conf.js
│ └── webpack.test.conf.js
├── config/
│ ├── dev.env.js
│ ├── index.js
│ ├── prod.env.js
│ ├── svgo-config.json
│ └── test.env.js
├── index.ejs
├── package.json
├── src/
│ ├── App.vue
│ ├── assets/
│ │ ├── icons/
│ │ │ └── index.js
│ │ └── scss/
│ │ ├── colors.scss
│ │ ├── common.scss
│ │ ├── frame.scss
│ │ ├── icon.scss
│ │ ├── layout.scss
│ │ ├── media.scss
│ │ ├── mixins.scss
│ │ ├── style.scss
│ │ ├── theme-element.scss
│ │ └── variables.scss
│ ├── components/
│ │ ├── CountUp.vue
│ │ ├── Elevator.vue
│ │ ├── Heart.vue
│ │ ├── HeartBroken.vue
│ │ ├── Icon/
│ │ │ ├── Icon.vue
│ │ │ └── index.js
│ │ ├── LoadMore.vue
│ │ ├── OfflineSeal.vue
│ │ ├── OperateTabs.vue
│ │ ├── Pagination.vue
│ │ ├── RecommendSeal.vue
│ │ ├── Search.vue
│ │ ├── SimilarRecommend.vue
│ │ ├── SubHead.vue
│ │ ├── SwitchTheme.vue
│ │ ├── UploadAvatar.vue
│ │ ├── Waline.vue
│ │ ├── dialog/
│ │ │ ├── AdBlockDialog.vue
│ │ │ ├── AutoDialog.vue
│ │ │ ├── EditDialog.vue
│ │ │ └── SentencesDialog.vue
│ │ ├── homepage/
│ │ │ ├── HomeLotus.vue
│ │ │ ├── Introduction.vue
│ │ │ ├── LinkCountup.vue
│ │ │ └── NiceFantasy.vue
│ │ ├── linksList/
│ │ │ ├── Index.vue
│ │ │ └── LinkItem.vue
│ │ ├── markdown/
│ │ │ ├── Index.vue
│ │ │ ├── PreviewMd.vue
│ │ │ └── markdown.css
│ │ └── sidebar/
│ │ ├── AdsPosition.vue
│ │ ├── AwesomeSentence.vue
│ │ ├── FriendsLinks.vue
│ │ ├── Main.vue
│ │ └── SitesRecommend.vue
│ ├── config/
│ │ ├── about.js
│ │ ├── classify.js
│ │ ├── constant.js
│ │ ├── default.js
│ │ ├── placeholder.js
│ │ ├── sentences.js
│ │ ├── tags.js
│ │ └── theme.js
│ ├── filters.js
│ ├── global.js
│ ├── helper/
│ │ ├── ajax.js
│ │ ├── apis.js
│ │ ├── auth.js
│ │ ├── document.js
│ │ ├── errorReport.js
│ │ ├── index.js
│ │ ├── marked.js
│ │ ├── system.js
│ │ ├── tool.js
│ │ ├── uploadAvatar.js
│ │ └── util.js
│ ├── locales/
│ │ └── zh.js
│ ├── main.js
│ ├── mixins/
│ │ ├── globalMixin.js
│ │ └── partsMixin.js
│ ├── partials/
│ │ ├── FooterNav.vue
│ │ ├── Frame.vue
│ │ ├── HeaderNav.vue
│ │ ├── Login.vue
│ │ ├── NotFound.vue
│ │ └── SideNav.vue
│ ├── router/
│ │ ├── beforeEachHooks.js
│ │ ├── commonRoutes.js
│ │ ├── index.js
│ │ └── routers/
│ │ ├── index.js
│ │ ├── mainRouter.js
│ │ └── manageRouter.js
│ ├── store/
│ │ ├── actions.js
│ │ ├── getters.js
│ │ ├── index.js
│ │ └── mutations.js
│ └── views/
│ ├── About.vue
│ ├── Account.vue
│ ├── Business.vue
│ ├── Cemetery.vue
│ ├── ForgotPwd.vue
│ ├── FriendLink.vue
│ ├── Homepage.vue
│ ├── Index.vue
│ ├── Nicelinks.vue
│ ├── Post.vue
│ ├── Recommend.vue
│ ├── Redirect.vue
│ ├── Setting.vue
│ ├── Sponsor.vue
│ ├── Tags.vue
│ ├── TagsCollections.vue
│ ├── Theme.vue
│ ├── ThemeCollections.vue
│ ├── manage/
│ │ ├── Adverts.vue
│ │ ├── Friends.vue
│ │ ├── Index.vue
│ │ ├── Links.vue
│ │ ├── Sentences.vue
│ │ └── Users.vue
│ └── share/
│ └── ShareLink.vue
├── static/
│ ├── .gitkeep
│ ├── css/
│ │ └── app.d6920e1d9405925de1b68384f6697dae.css
│ ├── img/
│ │ └── favicons/
│ │ ├── browserconfig.xml
│ │ └── manifest.json
│ ├── js/
│ │ ├── autotrack.js
│ │ ├── browsermodal.js
│ │ └── vendor.dll.js
│ └── manifest.json
└── test/
├── e2e/
│ ├── custom-assertions/
│ │ └── elementCount.js
│ ├── nightwatch.conf.js
│ ├── runner.js
│ └── specs/
│ └── test.js
├── index.js
└── unit/
├── .eslintrc
├── index.js
├── karma.conf.js
└── specs/
└── Hello.spec.js
Condensed preview — 165 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,028K chars).
[
{
"path": ".babelrc",
"chars": 186,
"preview": "{\n \"presets\": [\n [\"es2015\", { \"modules\": false }],\n \"stage-2\"\n ],\n \"plugins\": [\n \"transform-runtime\"\n ],..."
},
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_..."
},
{
"path": ".eslintignore",
"chars": 73,
"preview": "src/assets/js/*.js\nbuild/*.js\nconfig/*.js\nsrc/components/UploadAvatar.vue"
},
{
"path": ".eslintrc.js",
"chars": 642,
"preview": "// http://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n root: true,\n parser: 'babel-eslint',\n parserOpt..."
},
{
"path": ".gitignore",
"chars": 142,
"preview": ".DS_Store\nnode_modules/\ndist/\nbackups/\nnpm-debug.log\nyarn-error.log\ntest/unit/coverage\ntest/e2e/reports\nselenium-debug.l..."
},
{
"path": ".npmrc",
"chars": 42,
"preview": "registry=\"https://registry.npm.taobao.org\""
},
{
"path": ".prettierignore",
"chars": 80,
"preview": "node_modules\n\ndist\nbackups\n\nsrc/assets/element\nsrc/assets/scss/bootstrap\n\n.cache"
},
{
"path": "CHANGELOG.md",
"chars": 45,
"preview": "<a name=\"1.0.0\"></a>\n# 1.0.0 (2017-04-19)\n\n\n\n"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2018 JadeYang(杨琼璞)\n\nPermission is hereby granted, free of charge, to any person obtaining a c..."
},
{
"path": "README.md",
"chars": 4455,
"preview": "<h1 align=\"center\"><a href=\"https://site.lovejade.cn/?utm_source=github-nicelinks\"><img src=\"https://image.lovejade.cn/n..."
},
{
"path": "ads.txt",
"chars": 58,
"preview": "google.com, pub-8586652723015758, DIRECT, f08c47fec0942fa0"
},
{
"path": "build/build.js",
"chars": 1550,
"preview": "// https://github.com/shelljs/shelljs\nrequire('./check-versions')()\nrequire('shelljs/global')\n\nenv.NODE_ENV = process.en..."
},
{
"path": "build/check-versions.js",
"chars": 1172,
"preview": "var chalk = require('chalk')\nvar semver = require('semver')\nvar packageConfig = require('../package.json')\n\nfunction exe..."
},
{
"path": "build/deploy.js",
"chars": 761,
"preview": "var path = require('path')\nvar shell = require('shelljs')\nvar chalk = require('chalk')\n\nlet sourcePath = path.resolve(__..."
},
{
"path": "build/dev-client.js",
"chars": 245,
"preview": "/* eslint-disable */\nrequire('eventsource-polyfill')\nvar hotClient = require('webpack-hot-middleware/client?noInfo=true&..."
},
{
"path": "build/dev-server.js",
"chars": 2378,
"preview": "require('./check-versions')()\n\nvar config = require('../config')\nif (!process.env.NODE_ENV) {\n process.env.NODE_ENV = J..."
},
{
"path": "build/generate-sitemap.js",
"chars": 575,
"preview": "const sitemapGenerator = require('sitemap-generator');\nconst path = require('path')\n\n// create generator\nconst generator..."
},
{
"path": "build/load-minified.js",
"chars": 258,
"preview": "'use strict'\n\nconst fs = require('fs')\nconst UglifyJS = require('uglify-es')\n\nmodule.exports = function(filePath) {\n co..."
},
{
"path": "build/service-worker-dev.js",
"chars": 795,
"preview": "// This service worker file is effectively a 'no-op' that will reset any\n// previous service worker registered for the s..."
},
{
"path": "build/service-worker-prod.js",
"chars": 2335,
"preview": "(function() {\n 'use strict';\n\n // Check to make sure service workers are supported in the current browser,\n // and th..."
},
{
"path": "build/utils.js",
"chars": 1808,
"preview": "const path = require('path')\nconst config = require('../config')\nconst MiniCssExtractPlugin = require('mini-css-extract-..."
},
{
"path": "build/vendor-manifest.json",
"chars": 4700,
"preview": "{\"name\":\"vendor_library\",\"content\":{\"./node_modules/axios/lib/utils.js\":{\"id\":0,\"buildMeta\":{\"providedExports\":true}},\"...."
},
{
"path": "build/vue-loader.conf.js",
"chars": 398,
"preview": "var utils = require('./utils')\nvar config = require('../config')\nvar isProduction = process.env.NODE_ENV === 'production..."
},
{
"path": "build/webpack.base.conf.js",
"chars": 4756,
"preview": "const path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst webpack = requir..."
},
{
"path": "build/webpack.dev.conf.js",
"chars": 1458,
"preview": "const fs = require('fs')\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack'..."
},
{
"path": "build/webpack.dll.conf.js",
"chars": 2037,
"preview": "const path = require('path')\nconst webpack = require('webpack')\nconst TerserPlugin = require('terser-webpack-plugin')\nco..."
},
{
"path": "build/webpack.prod.conf.js",
"chars": 8419,
"preview": "const path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require(..."
},
{
"path": "build/webpack.test.conf.js",
"chars": 584,
"preview": "// This is the webpack config used for unit tests.\n\nvar utils = require('./utils')\nvar webpack = require('webpack')\nvar..."
},
{
"path": "config/dev.env.js",
"chars": 139,
"preview": "var merge = require('webpack-merge')\nvar prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEnv, {\n NODE_ENV:..."
},
{
"path": "config/index.js",
"chars": 1597,
"preview": "// see http://vuejs-templates.github.io/webpack for documentation.\nvar path = require('path')\n\nlet publicPathPrefix = pr..."
},
{
"path": "config/prod.env.js",
"chars": 48,
"preview": "module.exports = {\n NODE_ENV: '\"production\"'\n}\n"
},
{
"path": "config/svgo-config.json",
"chars": 1499,
"preview": "{\n \"plugins\": [\n {\n \"cleanupAttrs\": true\n },\n {\n \"cleanupEnableBackground\": true\n },\n {..."
},
{
"path": "config/test.env.js",
"chars": 132,
"preview": "var merge = require('webpack-merge')\nvar devEnv = require('./dev.env')\n\nmodule.exports = merge(devEnv, {\n NODE_ENV: '\"t..."
},
{
"path": "index.ejs",
"chars": 4672,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n <head>\n <meta charset=\"utf-8\">\n <title>倾城之链 | NICE LINKS</title>\n <meta n..."
},
{
"path": "package.json",
"chars": 4945,
"preview": "{\n \"name\": \"nicelinks-vue-client\",\n \"version\": \"3.11.3\",\n \"description\": \"A nice website for assembling nice links cr..."
},
{
"path": "src/App.vue",
"chars": 1376,
"preview": "<template>\n <div id=\"app\">\n <router-view></router-view>\n </div>\n</template>\n\n<script>\nimport 'element-ui/lib/theme-..."
},
{
"path": "src/assets/icons/index.js",
"chars": 187,
"preview": "const files = require.context('.', true, /\\.svg$/)\n\nconst modules = {}\n\nfiles.keys().forEach((key) => {\n modules[key.re..."
},
{
"path": "src/assets/scss/colors.scss",
"chars": 1324,
"preview": "$emotion: $jade;\n$encourage: #0099ff;\n$entertainment: #ff6666;\n$self-cultivation: #8552a1;\n$aestheticism: #2edfa3;\n$phil..."
},
{
"path": "src/assets/scss/common.scss",
"chars": 4469,
"preview": "* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\n*:focus {\n outline: none !important;\n}\n\nbody {\n font: norm..."
},
{
"path": "src/assets/scss/frame.scss",
"chars": 814,
"preview": ".header {\n font-weight: 500 !important;\n .menu {\n display: none;\n position: absolute;\n top: 0;\n left: 0;..."
},
{
"path": "src/assets/scss/icon.scss",
"chars": 132,
"preview": ".icons {\n width: 2.2rem;\n height: 2.2rem;\n margin: 0 0.88em;\n cursor: pointer;\n}\n.icon-heart {\n width: 2rem;\n heig..."
},
{
"path": "src/assets/scss/layout.scss",
"chars": 4892,
"preview": "#app {\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n text-align: center;\n color: #2c3e..."
},
{
"path": "src/assets/scss/media.scss",
"chars": 2222,
"preview": ".theme-jade-color {\n background-color: #313538;\n background-image: -webkit-radial-gradient(\n center,\n circle far..."
},
{
"path": "src/assets/scss/mixins.scss",
"chars": 1060,
"preview": "@mixin clearfix() {\n &:before,\n &:after {\n content: ' ';\n display: table;\n }\n &:after {\n clear: both;\n }\n}..."
},
{
"path": "src/assets/scss/style.scss",
"chars": 213,
"preview": "@import 'variables.scss';\n@import 'mixins.scss';\n@import 'common.scss';\n@import 'frame.scss';\n@import 'icon.scss';\n@impo..."
},
{
"path": "src/assets/scss/theme-element.scss",
"chars": 3233,
"preview": "#app {\n .el-input__inner,\n .el-textarea__inner {\n background-repeat: no-repeat;\n background-position: right 8px..."
},
{
"path": "src/assets/scss/variables.scss",
"chars": 1309,
"preview": "$brand: #ef4136;\n\n$white: #ffffff;\n$black: #17223b;\n$grey: #f7f8f9;\n$red: #fa0101;\n$jade: #34dfa5;\n$producthunt: #ea552d..."
},
{
"path": "src/components/CountUp.vue",
"chars": 1598,
"preview": "<template>\n <strong class=\"countup\"></strong>\n</template>\n\n<script>\nimport CountUpJs from 'countup'\n\nexport default {..."
},
{
"path": "src/components/Elevator.vue",
"chars": 3943,
"preview": "<template>\n <div class=\"elevator\">\n <div class=\"tooltip\">\n <div class=\"pannel\">\n <div class=\"item\">..."
},
{
"path": "src/components/Heart.vue",
"chars": 1410,
"preview": "<template functional>\n <div class=\"heart-wrap\">\n <div class=\"circle\"></div>\n <svg v-if=\"props.isDown\" class=\"icon..."
},
{
"path": "src/components/HeartBroken.vue",
"chars": 1119,
"preview": "<template functional>\n <div class=\"heart-wrap\">\n <svg\n v-if=\"props.isDown\"\n xmlns=\"http://www.w3.org/2000/..."
},
{
"path": "src/components/Icon/Icon.vue",
"chars": 439,
"preview": "<template>\n <svg class=\"icons\" :class=\"iconClass\">\n <use :xlink:href=\"Icons[name]\"></use>\n </svg>\n</template>\n\n<scr..."
},
{
"path": "src/components/Icon/index.js",
"chars": 39,
"preview": "module.exports = require('./Icon.vue')\n"
},
{
"path": "src/components/LoadMore.vue",
"chars": 1101,
"preview": "<template>\n <div class=\"load-more\">\n <el-button\n type=\"primary\"\n icon=\"plus\"\n size=\"large\"\n v-if..."
},
{
"path": "src/components/OfflineSeal.vue",
"chars": 105,
"preview": "<template functional>\n <div class=\"offline-seal\">\n <div class=\"seal\">\n </div>\n </div>\n</template>"
},
{
"path": "src/components/OperateTabs.vue",
"chars": 1576,
"preview": "<template>\n <div class=\"operate-tabs\" id=\"operate-tabs\">\n <div class=\"nav-wrap\">\n <a\n v-for=\"item in ite..."
},
{
"path": "src/components/Pagination.vue",
"chars": 3896,
"preview": "<template>\n <div class=\"pagination\" v-if=\"items.length > 0\">\n <ul class=\"pager\">\n <li class=\"item\" :class=\"{ di..."
},
{
"path": "src/components/RecommendSeal.vue",
"chars": 107,
"preview": "<template functional>\n <div class=\"recommend-seal\">\n <div class=\"seal\">\n </div>\n </div>\n</template>"
},
{
"path": "src/components/Search.vue",
"chars": 6009,
"preview": "<template>\n <el-autocomplete id=\"search-nice\" popper-class=\"search-autocomplete\" placement=\"bottom-start\"\n :select-w..."
},
{
"path": "src/components/SimilarRecommend.vue",
"chars": 5036,
"preview": "<template>\n <div class=\"similar-recommend\" v-if=\"linksArr.length > 0\">\n <h2 class=\"page-second-title\">猜您可能喜欢</h2>..."
},
{
"path": "src/components/SubHead.vue",
"chars": 3676,
"preview": "<template>\n <div class=\"sub-head\" id=\"sub-head\">\n <ul class=\"sub-head-nav\" ref=\"subHeadNav\">\n <li :class=\"makeC..."
},
{
"path": "src/components/SwitchTheme.vue",
"chars": 2601,
"preview": "<template>\n <div class=\"toggle-wrapper\">\n <div class=\"gg-sun btn\" @click=\"onThemeToggle\" v-if=\"isDarkMode\"></div>..."
},
{
"path": "src/components/UploadAvatar.vue",
"chars": 39945,
"preview": "<template>\n <div class=\"vue-image-crop-upload\" v-if=\"value\">\n <div class=\"vicp-wrap\">\n <div class=\"vicp-close\"..."
},
{
"path": "src/components/Waline.vue",
"chars": 2104,
"preview": "<template>\n <div class=\"nice-comment\">\n <h2 class=\"page-second-title\">\n <strong>精彩绝伦的评论</strong>\n </h2>..."
},
{
"path": "src/components/dialog/AdBlockDialog.vue",
"chars": 3611,
"preview": "<template>\n <div class=\"block-tip-dialog\">\n <div class=\"dlg-header\">\n <h2 class=\"title\">\n 若此弹框悬浮出来,多是由 A..."
},
{
"path": "src/components/dialog/AutoDialog.vue",
"chars": 3910,
"preview": "<template>\n <div class=\"auto-dialog\">\n <div class=\"dlg-header\">\n <h2 class=\"title\">\n 订阅倾城\n <a tar..."
},
{
"path": "src/components/dialog/EditDialog.vue",
"chars": 7094,
"preview": "<template>\n <div id=\"edit-dialog\">\n <el-dialog stripe :title=\"$t('shareNewLink')\" :visible.sync=\"isShowDlgFlag\" v-if..."
},
{
"path": "src/components/dialog/SentencesDialog.vue",
"chars": 4376,
"preview": "<template>\n <div class=\"sentences-dialog\">\n <el-dialog stripe :title=\"$t('shareNewSentences')\" :visible.sync=\"isShow..."
},
{
"path": "src/components/homepage/HomeLotus.vue",
"chars": 11246,
"preview": "<template>\n <section class=\"lotus fade-in animated\" id=\"lotus\">\n <article class=\"twelve columns lotus-animations\">..."
},
{
"path": "src/components/homepage/Introduction.vue",
"chars": 2131,
"preview": "<template>\n <section>\n <article class=\"introduction\">\n <h2 class=\"find-title medium-font\">探索更广阔的世界,为您</h2>..."
},
{
"path": "src/components/homepage/LinkCountup.vue",
"chars": 2682,
"preview": "<template>\n <div class=\"countup-area\">\n <div class=\"top-crescent\" v-if=\"!$isMobile\"></div>\n <h2 class=\"countup-ti..."
},
{
"path": "src/components/homepage/NiceFantasy.vue",
"chars": 1339,
"preview": "<template>\n <section class=\"nice-fantasy\">\n <img class=\"hero\" :src=\"niceImageSrc\" :alt=\"$t('niceLinksStr')\" />\n </s..."
},
{
"path": "src/components/linksList/Index.vue",
"chars": 2863,
"preview": "<template>\n <div class=\"links-list\">\n <el-card shadow=\"hover\" v-if=\"pdata.length <= 0\">\n <div class=\"content\">..."
},
{
"path": "src/components/linksList/LinkItem.vue",
"chars": 17138,
"preview": "<template>\n <div class=\"content\">\n <div class=\"info-block mb-normal\" v-if=\"!isAbstract\">\n <a class=\"user-info\"..."
},
{
"path": "src/components/markdown/Index.vue",
"chars": 1797,
"preview": "<template>\n <div class=\"jade-markdown\">\n <el-tabs v-model=\"activeName\" type=\"card\" @tab-click=\"onHandleClick\">..."
},
{
"path": "src/components/markdown/PreviewMd.vue",
"chars": 352,
"preview": "<template>\n <div class=\"markdown-body\" v-html=\"beParsedVal\" />\n</template>\n\n<script>\nimport { parse } from 'helper/mark..."
},
{
"path": "src/components/markdown/markdown.css",
"chars": 4706,
"preview": ".markdown-body ol ol,\n.markdown-body ul ol,\n.markdown-body ol ul,\n.markdown-body ul ul,\n.markdown-body ol ul ol,\n.markdo..."
},
{
"path": "src/components/sidebar/AdsPosition.vue",
"chars": 925,
"preview": "<template functional>\n <aside class=\"sidebar-aside\" id=\"jade-gg-position\">\n <h2 class=\"aside-widget-title\" style=\"ma..."
},
{
"path": "src/components/sidebar/AwesomeSentence.vue",
"chars": 6423,
"preview": "<template>\n <aside class=\"sidebar-aside\" id=\"awesome-sentence\">\n <h2 class=\"aside-widget-title\">{{ $t('awesomeSenten..."
},
{
"path": "src/components/sidebar/FriendsLinks.vue",
"chars": 1738,
"preview": "<template>\n <aside class=\"sidebar-aside\">\n <h2 class=\"aside-widget-title\">{{ $t('friendsLinks') }}</h2>\n <ul clas..."
},
{
"path": "src/components/sidebar/Main.vue",
"chars": 3125,
"preview": "<template>\n <div class=\"sidebar\">\n <aside class=\"sidebar-aside\">\n <h2 class=\"aside-widget-title sidebar-recomme..."
},
{
"path": "src/components/sidebar/SitesRecommend.vue",
"chars": 977,
"preview": "<template>\n <aside class=\"sidebar-aside\" id=\"sites-recommend\">\n <h2 class=\"aside-widget-title\">推荐投稿</h2>\n <a..."
},
{
"path": "src/config/about.js",
"chars": 2669,
"preview": "export default `\n[「倾城之链」](https://nicelinks.site/?utm_source=nicelinks.site),作为一个开放平台,旨在云集全球优秀网站,探索互联网中更广阔的世界;在这里,你可以轻松发..."
},
{
"path": "src/config/classify.js",
"chars": 211,
"preview": "export default Object.freeze([\n {\n name: 'skill',\n value: '0',\n },\n {\n name: 'resource',\n value: '1',\n }..."
},
{
"path": "src/config/constant.js",
"chars": 516,
"preview": "export const STORAGE_PREFIX = 'ns-storage'\n\nexport const AUTO_DIALOG = `auto-dialog-${new Date().getFullYear()}`\n\nexport..."
},
{
"path": "src/config/default.js",
"chars": 2096,
"preview": "export default [\n {\n _id: '59ba84083df6765c75b77919',\n urlPath: 'https://500px.com/',\n title: 'The Premier Pho..."
},
{
"path": "src/config/placeholder.js",
"chars": 913,
"preview": "export default [\n {\n height: '2rem',\n boxes: [[0, '30%']],\n },\n {\n height: '1rem',\n boxes: [[0, 0]],\n },..."
},
{
"path": "src/config/sentences.js",
"chars": 326,
"preview": "export default [\n {\n text: '情感',\n value: 'emotion',\n },\n {\n text: '励志',\n value: 'encourage',\n },\n {..."
},
{
"path": "src/config/tags.js",
"chars": 2965,
"preview": "export default Object.freeze([\n '免费',\n '灵感',\n '产品',\n '发现',\n '官网',\n '导航',\n '美图',\n '博客',\n '旅行',\n '信息',\n '前端开发',..."
},
{
"path": "src/config/theme.js",
"chars": 1016,
"preview": "export default Object.freeze([\n // 0: 技术客栈\n [\n { key: '程序', value: 'Program' },\n { key: '设计', value: 'Design' },..."
},
{
"path": "src/filters.js",
"chars": 111,
"preview": "export default {\n dateConvert(time) {\n return time ? new Date(time).Format('yyyy-MM-dd hh:mm') : ''\n },\n}\n"
},
{
"path": "src/global.js",
"chars": 2305,
"preview": "import Vue from 'vue'\nimport cloneDeep from 'lodash/cloneDeep'\nimport {\n Pagination,\n Dialog,\n Input,\n Switch,\n Sel..."
},
{
"path": "src/helper/ajax.js",
"chars": 1661,
"preview": "import axios from 'axios'\nimport $errorReport from './errorReport'\nimport { setCurrentDate } from './tool'\n\nfunction que..."
},
{
"path": "src/helper/apis.js",
"chars": 4194,
"preview": "import $ajax from './ajax'\n\nfunction requestUrl(path) {\n return `/api/${path}`\n}\n\nexport default {\n getNiceLinks(data)..."
},
{
"path": "src/helper/auth.js",
"chars": 602,
"preview": "import Cookies from 'js-cookie'\nimport { $apis } from 'helper'\nimport { getSessionStorage } from './../helper/tool'\n\nexp..."
},
{
"path": "src/helper/document.js",
"chars": 967,
"preview": "export const toggleClass = (el = {}, className) => {\n if (el.classList) {\n el.classList.toggle(className)\n } else {..."
},
{
"path": "src/helper/errorReport.js",
"chars": 518,
"preview": "import Vue from 'vue'\nimport Raven from 'raven-js'\nimport RavenVue from 'raven-js/plugins/vue'\n\nconst sentryUrl = 'https..."
},
{
"path": "src/helper/index.js",
"chars": 94,
"preview": "export const $util = require('./util').default\nexport const $apis = require('./apis').default\n"
},
{
"path": "src/helper/marked.js",
"chars": 370,
"preview": "import { marked } from 'marked'\n\nconst renderer = new marked.Renderer()\nconst linkRenderer = renderer.link\nrenderer.link..."
},
{
"path": "src/helper/system.js",
"chars": 1353,
"preview": "export const isAndroidSystem = () => {\n const ua = window.navigator.userAgent\n return ua.indexOf('Android') > -1 || ua..."
},
{
"path": "src/helper/tool.js",
"chars": 6222,
"preview": "// @desc:分流 utils 中未被频繁使用的方法,同时以模块化输出;\nimport sha256 from 'crypto-js/sha256'\nimport md5 from 'crypto-js/md5'\nimport { ST..."
},
{
"path": "src/helper/uploadAvatar.js",
"chars": 2698,
"preview": "export default {\n /**\n * database64文件格式转换为2进制\n *\n * @param {[String]} data dataURL 的格式为 “data:image/png;base64,*..."
},
{
"path": "src/helper/util.js",
"chars": 1963,
"preview": "if (typeof String.prototype.startsWith !== 'function') {\n window.String.prototype.startsWith = function (prefix) {..."
},
{
"path": "src/locales/zh.js",
"chars": 2680,
"preview": "import { DESCRIPTION } from './../config/constant'\n\nexport default {\n signIn: '登录',\n signUp: '注册',\n signOut: '退出登录',..."
},
{
"path": "src/main.js",
"chars": 838,
"preview": "import Vue from 'vue'\nimport './global.js'\nimport './mixins/globalMixin.js'\nimport App from './App'\nimport router from '..."
},
{
"path": "src/mixins/globalMixin.js",
"chars": 1884,
"preview": "import Vue from 'vue'\nimport $auth from './../helper/auth'\nimport { mapActions, mapMutations } from 'vuex'\nimport { isLe..."
},
{
"path": "src/mixins/partsMixin.js",
"chars": 3282,
"preview": "import { mapMutations } from 'vuex'\n\nimport { updateAfterFilterEmptyValue } from './../helper/tool'\nimport CLASSIFY_CONF..."
},
{
"path": "src/partials/FooterNav.vue",
"chars": 6322,
"preview": "<template>\n <footer class=\"footer\">\n <div class=\"footer-body\">\n <div class=\"container\">\n <div class=\"lis..."
},
{
"path": "src/partials/Frame.vue",
"chars": 4744,
"preview": "<template>\n <div class=\"page-wrap\">\n <header-nav></header-nav>\n <side-nav v-if=\"$isMobile\"></side-nav>\n <main..."
},
{
"path": "src/partials/HeaderNav.vue",
"chars": 15350,
"preview": "<template>\n <header class=\"header blur-effect\">\n <nav class=\"nav\">\n <div class=\"header-logo\">\n <a href=\"..."
},
{
"path": "src/partials/Login.vue",
"chars": 15122,
"preview": "<template>\n <div class=\"login-wrap\">\n <div class=\"wechat-box\" ref=\"wechat-box\">\n <h2 class=\"box-title\">微信扫码登录/注..."
},
{
"path": "src/partials/NotFound.vue",
"chars": 5778,
"preview": "<template>\n <div class=\"error-404\">\n <div class=\"error-404-body\">\n <el-card>\n <div class=\"error-404-body..."
},
{
"path": "src/partials/SideNav.vue",
"chars": 5865,
"preview": "<template>\n <div class=\"side-nav\">\n <el-menu :default-openeds=\"[]\" default-active=\"1\" :unique-opened=\"true\" @open=\"h..."
},
{
"path": "src/router/beforeEachHooks.js",
"chars": 425,
"preview": "/*\n * @desc: Check routing jump permission control.\n */\n\nimport $auth from './../helper/auth'\n\nexport default {\n checkV..."
},
{
"path": "src/router/commonRoutes.js",
"chars": 556,
"preview": "import NotFound from 'partials/NotFound'\nimport routesMap from './routers'\n\nlet commonConf = [\n {\n path: '/login',..."
},
{
"path": "src/router/index.js",
"chars": 499,
"preview": "import Vue from 'vue'\nimport Router from 'vue-router'\nimport beforeEachHooks from './beforeEachHooks'\nimport routesMapCo..."
},
{
"path": "src/router/routers/index.js",
"chars": 199,
"preview": "const files = require.context('.', true, /\\.js$/)\nconst moudles = []\n\nfiles.keys().forEach((key) => {\n if (key === './i..."
},
{
"path": "src/router/routers/mainRouter.js",
"chars": 2776,
"preview": "import Frame from 'partials/Frame'\n\nexport default {\n path: '/',\n component: Frame,\n children: [\n {\n path: '/..."
},
{
"path": "src/router/routers/manageRouter.js",
"chars": 1092,
"preview": "import Frame from 'partials/Frame'\n\nexport default {\n path: '/manage',\n component: Frame,\n meta: {\n isNeedAuth: tr..."
},
{
"path": "src/store/actions.js",
"chars": 341,
"preview": "import { $apis } from 'helper'\nimport Cookies from 'js-cookie'\n\nexport default {\n async $getUserInfo({ commit, state })..."
},
{
"path": "src/store/getters.js",
"chars": 70,
"preview": "export default {\n userInfo(state) {\n return state.userInfo\n },\n}\n"
},
{
"path": "src/store/index.js",
"chars": 1081,
"preview": "/*\n FixBug: [vuex] already installed. Vue.use(Vuex) should be called only once.\n Details: https://github.com/vuejs/vue..."
},
{
"path": "src/store/mutations.js",
"chars": 1057,
"preview": "import Cookies from 'js-cookie'\nimport { PAGE_SIZE } from 'config/constant'\n\nexport default {\n $vuexSetUserInfo(state,..."
},
{
"path": "src/views/About.vue",
"chars": 1069,
"preview": "<template>\n <div class=\"wrapper account\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">\n <div c..."
},
{
"path": "src/views/Account.vue",
"chars": 1519,
"preview": "<template>\n <div class=\"wrapper account\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <div class=\"..."
},
{
"path": "src/views/Business.vue",
"chars": 1878,
"preview": "<template>\n <div class=\"wrapper account\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">\n <div c..."
},
{
"path": "src/views/Cemetery.vue",
"chars": 2769,
"preview": "<template>\n <div class=\"wrapper\" id=\"cemetery\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">..."
},
{
"path": "src/views/ForgotPwd.vue",
"chars": 4611,
"preview": "<template>\n <div class=\"wrapper\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <div class=\"panel-bo..."
},
{
"path": "src/views/FriendLink.vue",
"chars": 5176,
"preview": "<template>\n <div class=\"wrapper account\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">\n <div c..."
},
{
"path": "src/views/Homepage.vue",
"chars": 8668,
"preview": "<template>\n <div class=\"wrapper homepage\">\n <div class=\"panel-default\" v-loading=\"isLoading\">\n <div class=\"pane..."
},
{
"path": "src/views/Index.vue",
"chars": 660,
"preview": "<template>\n <div class=\"wrapper\">\n <search class=\"mobile-search\" v-if=\"$isMobile\" />\n <HomeLotus :class=\"{ 'mobil..."
},
{
"path": "src/views/Nicelinks.vue",
"chars": 2544,
"preview": "<template>\n <div class=\"wrapper\" id=\"nice-links\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">..."
},
{
"path": "src/views/Post.vue",
"chars": 4524,
"preview": "<template>\n <div id=\"post-page\" class=\"wrapper\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">..."
},
{
"path": "src/views/Recommend.vue",
"chars": 2723,
"preview": "<template>\n <div class=\"wrapper\" id=\"recommend\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">..."
},
{
"path": "src/views/Redirect.vue",
"chars": 5148,
"preview": "<template>\n <div class=\"redirect-wrapper\">\n <h1 class=\"jump-title\">\n <img\n class=\"logo\"\n src=\"/st..."
},
{
"path": "src/views/Setting.vue",
"chars": 8450,
"preview": "<template>\n <div class=\"wrapper\" id=\"setting\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <div cl..."
},
{
"path": "src/views/Sponsor.vue",
"chars": 2481,
"preview": "<template>\n <div class=\"wrapper sponsor\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">\n <div c..."
},
{
"path": "src/views/Tags.vue",
"chars": 853,
"preview": "<template>\n <div id=\"tags-page\" class=\"wrapper\">\n <div class=\"panel-default\" v-loading=\"isLoading\">\n <div class..."
},
{
"path": "src/views/TagsCollections.vue",
"chars": 1425,
"preview": "<template>\n <div id=\"tags-coll-page\" class=\"wrapper\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">..."
},
{
"path": "src/views/Theme.vue",
"chars": 1947,
"preview": "<template>\n <div id=\"theme-page\" class=\"wrapper\">\n <div class=\"panel-default\" v-loading=\"isLoading\">\n <div clas..."
},
{
"path": "src/views/ThemeCollections.vue",
"chars": 1425,
"preview": "<template>\n <div id=\"theme-coll-page\" class=\"wrapper\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">..."
},
{
"path": "src/views/manage/Adverts.vue",
"chars": 5311,
"preview": "<template>\n <div class=\"wrapper\" id=\"manage-users\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <d..."
},
{
"path": "src/views/manage/Friends.vue",
"chars": 5043,
"preview": "<template>\n <div class=\"wrapper\" id=\"manage-users\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <d..."
},
{
"path": "src/views/manage/Index.vue",
"chars": 711,
"preview": "<template>\n <div class=\"wrapper\" id=\"manage-index\">\n <div class=\"panel-default\">\n <div class=\"panel-body\">..."
},
{
"path": "src/views/manage/Links.vue",
"chars": 6422,
"preview": "<template>\n <div class=\"wrapper\" id=\"manage-links\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <d..."
},
{
"path": "src/views/manage/Sentences.vue",
"chars": 6618,
"preview": "<template>\n <div class=\"wrapper\" id=\"manage-sentences\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">..."
},
{
"path": "src/views/manage/Users.vue",
"chars": 5449,
"preview": "<template>\n <div class=\"wrapper\" id=\"manage-users\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <d..."
},
{
"path": "src/views/share/ShareLink.vue",
"chars": 9824,
"preview": "<template>\n <div id=\"share-link\" class=\"wrapper\">\n <div class=\"panel-default\" v-loading.body=\"isLoading\">\n <div..."
},
{
"path": "static/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "static/css/app.d6920e1d9405925de1b68384f6697dae.css",
"chars": 179594,
"preview": ".el-breadcrumb:after,.el-breadcrumb:before,.el-button-group:after,.el-button-group:before,.el-form-item:after,.el-form-i..."
},
{
"path": "static/img/favicons/browserconfig.xml",
"chars": 246,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig>\n <msapplication>\n <tile>\n <square150x150logo..."
},
{
"path": "static/img/favicons/manifest.json",
"chars": 513,
"preview": "{\n \"name\": \"\\u503e\\u57ce\\u4e4b\\u94fe\",\n \"short_name\": \"\\u503e\\u57ce\\u4e4b\\u94fe\",\n \"icons\": [\n {..."
},
{
"path": "static/js/autotrack.js",
"chars": 26091,
"preview": "(function () {\n var f, aa = typeof Object.defineProperties === 'function' ? Object.defineProperty : function (a, b, c)..."
},
{
"path": "static/js/browsermodal.js",
"chars": 5279,
"preview": "let tpl = '' +\n'<style type=\"text/css\" scoped>' +\n'body{' +\n' margin: 0;' +\n' padding: 0;' +\n'}' +\n'' +\n'.modal-browse..."
},
{
"path": "static/js/vendor.dll.js",
"chars": 340332,
"preview": "var vendor_library=function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};retur..."
},
{
"path": "static/manifest.json",
"chars": 576,
"preview": "{\n \"name\": \"\\u503e\\u57ce\\u4e4b\\u94fe\",\n \"short_name\": \"\\u503e\\u57ce\\u4e4b\\u94fe\",\n \"icons\": [\n {..."
},
{
"path": "test/e2e/custom-assertions/elementCount.js",
"chars": 777,
"preview": "// A custom Nightwatch assertion.\n// the name of the method is the filename.\n// can be used in tests like this:\n//\n//..."
},
{
"path": "test/e2e/nightwatch.conf.js",
"chars": 1019,
"preview": "require('babel-register')\nvar config = require('../../config')\n\n// http://nightwatchjs.org/guide#settings-file\nmodule.ex..."
},
{
"path": "test/e2e/runner.js",
"chars": 1009,
"preview": "// 1. start the dev server using production config\nprocess.env.NODE_ENV = 'testing'\nvar server = require('../../build/de..."
},
{
"path": "test/e2e/specs/test.js",
"chars": 561,
"preview": "// For authoring Nightwatch tests, see\n// http://nightwatchjs.org/guide#usage\n\nmodule.exports = {\n 'default e2e tests':..."
},
{
"path": "test/index.js",
"chars": 709,
"preview": "/*\n Date: @2017-10-16\n Desc: 以 Node 本地服务器,运行打包到 Dist 中的代码(❗️:npm run pretest)\n*/\n\nlet express = require('express')\nlet..."
},
{
"path": "test/unit/.eslintrc",
"chars": 95,
"preview": "{\n \"env\": {\n \"mocha\": true\n },\n \"globals\": {\n \"expect\": true,\n \"sinon\": true\n }\n}\n"
},
{
"path": "test/unit/index.js",
"chars": 550,
"preview": "// Polyfill fn.bind() for PhantomJS\n/* eslint-disable no-extend-native */\nFunction.prototype.bind = require('function-bi..."
},
{
"path": "test/unit/karma.conf.js",
"chars": 974,
"preview": "// This is a karma config file. For more details see\n// http://karma-runner.github.io/0.13/config/configuration-file.h..."
},
{
"path": "test/unit/specs/Hello.spec.js",
"chars": 337,
"preview": "import Vue from 'vue'\nimport Hello from 'src/components/Hello'\n\ndescribe('Hello.vue', () => {\n it('should render correc..."
}
]
About this extraction
This page contains the full source code of the nicejade/nicelinks-vue-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 165 files (963.1 KB), approximately 328.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.