完成基础布局
parent
59157e181b
commit
a82c5391c2
|
@ -12,7 +12,7 @@
|
|||
<script type="importmap">{{importmap}}</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app noselect"></div>
|
||||
<div class="app"></div>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg width="41" height="41" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M37.532 16.87a9.963 9.963 0 00-.856-8.184 10.078 10.078 0 00-10.855-4.835A9.964 9.964 0 0018.306.5a10.079 10.079 0 00-9.614 6.977 9.967 9.967 0 00-6.664 4.834 10.08 10.08 0 001.24 11.817 9.965 9.965 0 00.856 8.185 10.079 10.079 0 0010.855 4.835 9.965 9.965 0 007.516 3.35 10.078 10.078 0 009.617-6.981 9.967 9.967 0 006.663-4.834 10.079 10.079 0 00-1.243-11.813zM22.498 37.886a7.474 7.474 0 01-4.799-1.735c.061-.033.168-.091.237-.134l7.964-4.6a1.294 1.294 0 00.655-1.134V19.054l3.366 1.944a.12.12 0 01.066.092v9.299a7.505 7.505 0 01-7.49 7.496zM6.392 31.006a7.471 7.471 0 01-.894-5.023c.06.036.162.099.237.141l7.964 4.6a1.297 1.297 0 001.308 0l9.724-5.614v3.888a.12.12 0 01-.048.103l-8.051 4.649a7.504 7.504 0 01-10.24-2.744zM4.297 13.62A7.469 7.469 0 018.2 10.333c0 .068-.004.19-.004.274v9.201a1.294 1.294 0 00.654 1.132l9.723 5.614-3.366 1.944a.12.12 0 01-.114.01L7.04 23.856a7.504 7.504 0 01-2.743-10.237zm27.658 6.437l-9.724-5.615 3.367-1.943a.121.121 0 01.113-.01l8.052 4.648a7.498 7.498 0 01-1.158 13.528v-9.476a1.293 1.293 0 00-.65-1.132zm3.35-5.043c-.059-.037-.162-.099-.236-.141l-7.965-4.6a1.298 1.298 0 00-1.308 0l-9.723 5.614v-3.888a.12.12 0 01.048-.103l8.05-4.645a7.497 7.497 0 0111.135 7.763zm-21.063 6.929l-3.367-1.944a.12.12 0 01-.065-.092v-9.299a7.497 7.497 0 0112.293-5.756 6.94 6.94 0 00-.236.134l-7.965 4.6a1.294 1.294 0 00-.654 1.132l-.006 11.225zm1.829-3.943l4.33-2.501 4.332 2.5v5l-4.331 2.5-4.331-2.5V18z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 1.5 KiB |
70
src/app.vue
70
src/app.vue
|
@ -1,63 +1,45 @@
|
|||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="/assets/logo.svg" width="125" height="125" />
|
||||
|
||||
<div class="wrapper">
|
||||
<Hello msg="It works!!!" />
|
||||
|
||||
<nav>
|
||||
<router-link to="/">Home</router-link>
|
||||
<router-link to="/about">About</router-link>
|
||||
</nav>
|
||||
<Topbar />
|
||||
<div class="main">
|
||||
<Aside />
|
||||
<Chat />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Hello from './components/hello.vue'
|
||||
import Topbar from './views/topbar.vue'
|
||||
import Aside from './views/aside.vue'
|
||||
import Chat from './views/chat.vue'
|
||||
|
||||
export default {
|
||||
components: { Hello }
|
||||
components: { Topbar, Aside, Chat }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.app {
|
||||
padding: 16px;
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
padding: 32px;
|
||||
font-size: 14px;
|
||||
color: var(--color-dark-1);
|
||||
}
|
||||
|
||||
header {
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-teal-1);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-teal-3);
|
||||
}
|
||||
|
||||
nav {
|
||||
margin-top: 32px;
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
margin: 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
margin: 32px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg viewBox="0 0 261.76 226.69" xmlns="http://www.w3.org/2000/svg"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/><path d="M36.21 192.639l160.921-74.805-81.778-5.063 119.519-67.69L49.06 126.138l88.8 2.712z" fill="rgb(252, 118, 97)"/></svg>
|
Before Width: | Height: | Size: 394 B |
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div v-if="name" class="avatar name">{{ name.slice(-1) }}</div>
|
||||
<img v-else class="avatar" src="/img/chatgpt.svg" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
name: ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-dark-1);
|
||||
color: #fff;
|
||||
|
||||
&.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4px;
|
||||
font-size: 18px;
|
||||
background: var(--color-orange-1);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,40 +0,0 @@
|
|||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
你已经成功运行了一个项目, 项目基于
|
||||
<a href="//github.com/bytedo/vue-live" target="_blank">Vue-live</a> +
|
||||
<a href="//vuejs.org" target="_blank">Vue 3</a>.
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
msg: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
font-size: 52px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.green {
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
color: var(--color-blue-1);
|
||||
}
|
||||
|
||||
.greetings {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
*
|
||||
* @author yutent<yutent.io@gmail.com>
|
||||
* @date 2021/03/24 11:50:09
|
||||
*/
|
||||
import fetch from 'fetch'
|
||||
|
||||
fetch.BASE_URL = localStorage.getItem('BASE_URL')
|
||||
|
||||
fetch.inject.request(conf => {
|
||||
conf.headers['content-type'] = 'application/json'
|
||||
conf.timeout = 60 * 1000
|
||||
})
|
||||
|
||||
fetch.inject.response(res => res.json())
|
||||
|
||||
export function post(url, body = {}) {
|
||||
return fetch(url, {
|
||||
method: 'post',
|
||||
body
|
||||
})
|
||||
}
|
||||
export function get(url, body = {}) {
|
||||
return fetch(url, { body })
|
||||
}
|
||||
|
||||
export default fetch
|
|
@ -0,0 +1,586 @@
|
|||
/**
|
||||
* markdown解析器
|
||||
* @author yutent<yutent.io@gmail.com>
|
||||
* @date 2020/02/07 17:14:19
|
||||
*/
|
||||
|
||||
const HR_LIST = ['=', '-', '_', '*']
|
||||
const LIST_RE = /^(([\+\-\*])|(\d+\.))\s/
|
||||
const TODO_RE = /^[\+\-\*]\s\[(x|\s)\]\s/
|
||||
const ESCAPE_RE = /\\([-+*_`\]\[\(\)])/g
|
||||
const QLINK_RE = /^\[(\d+)\]: ([\S]+)\s*?((['"])[\s\S]*?\4)?\s*?$/
|
||||
const TAG_RE = /<([\w\-]+)([\w\W]*?)>/g
|
||||
const ATTR_RE = /\s*?on[a-zA-Z]+="[^"]*?"\s*?/g
|
||||
const CODEBLOCK_RE = /```(.*?)([\w\W]*?)```/g
|
||||
const BLOCK_RE = /<([\w\-]+)([^>]*?)>([\w\W]*?)<\/\1>/g
|
||||
const IS_DOM_RE = /^<([\w\-]+)[^>]*?>.*?<\/\1>$/
|
||||
const STYLE_RE = /<style[^>]*?>([\w\W]*?)<\/style>/g
|
||||
|
||||
const INLINE = {
|
||||
code: /`([^`]*?[^`\\\s])`/g,
|
||||
strong: [/__([\s\S]*?[^\s\\])__(?!_)/g, /\*\*([\s\S]*?[^\s\\])\*\*(?!\*)/g],
|
||||
em: [/_([\s\S]*?[^\s\\])_(?!_)/g, /\*([\s\S]*?[^\s\\*])\*(?!\*)/g],
|
||||
del: /~~([\s\S]*?[^\s\\~])~~/g,
|
||||
qlink: /\[([^\]]*?)\]\[(\d*?)\]/g, // 引用链接
|
||||
img: /\!\[([^\]]*?)\]\(([^)]*?)\)/g,
|
||||
a: /\[([^\]]*?)\]\(([^)]*?)(\s+"([\s\S]*?)")*?\)/g,
|
||||
qlist: /((<blockquote class="md\-quote">)*?)([\+\-\*]|\d+\.) (.*)/ // 引用中的列表
|
||||
}
|
||||
|
||||
const ATTR_BR_SYMBOL = '⨨☇'
|
||||
const NODE_BR_SYMBOL = '⨨⤶'
|
||||
const ATTR_BR_EXP = new RegExp(ATTR_BR_SYMBOL, 'g')
|
||||
const NODE_BR_EXP = new RegExp(NODE_BR_SYMBOL, 'g')
|
||||
|
||||
const Helper = {
|
||||
// 是否分割线
|
||||
isHr(str) {
|
||||
var s = str[0]
|
||||
if (HR_LIST.includes(s)) {
|
||||
return str.slice(0, 3) === s.repeat(3) ? str.slice(3) : false
|
||||
}
|
||||
return false
|
||||
},
|
||||
// 是否列表, -1不是, 1为有序列表, 0为无序列表
|
||||
isList(str) {
|
||||
var v = str.trim()
|
||||
if (LIST_RE.test(v)) {
|
||||
var n = +v[0]
|
||||
if (n === n) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return -1
|
||||
},
|
||||
// 是否任务列表
|
||||
isTodo(str) {
|
||||
var v = str.trim()
|
||||
if (TODO_RE.test(v)) {
|
||||
return v[3] === 'x' ? 1 : 0
|
||||
}
|
||||
return -1
|
||||
},
|
||||
ltrim(str) {
|
||||
if (str.trimStart) {
|
||||
return str.trimStart()
|
||||
}
|
||||
return str.replace(/^\s+/, '')
|
||||
},
|
||||
isQLink(str) {
|
||||
if (QLINK_RE.test(str)) {
|
||||
// l: link, t: title, $1: index
|
||||
return { [RegExp.$1]: { l: RegExp.$2, t: RegExp.$3 } }
|
||||
}
|
||||
return false
|
||||
},
|
||||
isTable(str) {
|
||||
return /^\|.+?\|$/.test(str)
|
||||
},
|
||||
// 是否原生dom节点
|
||||
isNativeDom(str) {
|
||||
return IS_DOM_RE.test(str)
|
||||
}
|
||||
}
|
||||
|
||||
const Decoder = {
|
||||
// 内联样式
|
||||
inline(str) {
|
||||
return str
|
||||
.replace(INLINE.code, '<code class="inline">$1</code>')
|
||||
.replace(INLINE.strong[0], '<strong>$1</strong>')
|
||||
.replace(INLINE.strong[1], '<strong>$1</strong>')
|
||||
.replace(INLINE.em[0], '<em>$1</em>')
|
||||
.replace(INLINE.em[1], '<em>$1</em>')
|
||||
.replace(INLINE.del, '<del>$1</del>')
|
||||
.replace(INLINE.img, '<img src="$2" alt="$1">')
|
||||
.replace(INLINE.a, (m1, txt, link, m2, attr = '') => {
|
||||
var tmp = attr
|
||||
.split(';')
|
||||
.filter(_ => _)
|
||||
.map(_ => {
|
||||
var a = _.split('=')
|
||||
if (a.length > 1) {
|
||||
return `${a[0]}="${a[1]}"`
|
||||
} else {
|
||||
return `title="${_}"`
|
||||
}
|
||||
})
|
||||
.join(' ')
|
||||
|
||||
return `<a href="${link.trim()}" ${tmp}>${txt}</a>`
|
||||
})
|
||||
.replace(INLINE.qlink, (m, txt, n) => {
|
||||
var _ = this.__LINKS__[n]
|
||||
if (_) {
|
||||
var a = _.t ? `title=${_.t}` : ''
|
||||
return `<a href="${_.l}" ${a}>${txt}</a>`
|
||||
} else {
|
||||
return m
|
||||
}
|
||||
})
|
||||
.replace(ESCAPE_RE, '$1') // 处理转义字符
|
||||
},
|
||||
// 分割线
|
||||
hr(name = '') {
|
||||
return `<fieldset class="md-hr"><legend name="${name}"></legend></fieldset>`
|
||||
},
|
||||
// 标题
|
||||
head(str) {
|
||||
if (str.startsWith('#')) {
|
||||
return str.replace(/^(#{1,6}) (.*)/, (p, m1, m2) => {
|
||||
m2 = m2.trim()
|
||||
let level = m1.trim().length
|
||||
let hash = m2.replace(/\s/g, '').replace(/<\/?[^>]*?>/g, '')
|
||||
|
||||
if (level === 1) {
|
||||
return `<h1>${m2}</h1>`
|
||||
} else {
|
||||
return `<h${level}><a href="#${hash}" id="${hash}" class="md-head-link">${m2}</a></h${level}>`
|
||||
}
|
||||
})
|
||||
}
|
||||
return false
|
||||
},
|
||||
// 引用模块
|
||||
blockquote(str) {
|
||||
//
|
||||
},
|
||||
// 任务
|
||||
task(str) {
|
||||
var todoChecked = Helper.isTodo(str)
|
||||
if (~todoChecked) {
|
||||
var word = str.replace(TODO_RE, '').trim()
|
||||
var stat = todoChecked === 1 ? 'checked' : ''
|
||||
var txt = todoChecked === 1 ? `<del>${word}</del>` : word
|
||||
|
||||
return `<section><wc-checkbox readonly ${stat}>${txt}</wc-checkbox></section>`
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function fixed(str) {
|
||||
// 去掉\r, 将\t转为空格(2个)
|
||||
return str
|
||||
.replace(/\r\n|\r/g, '\n')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/\u00a0/g, ' ')
|
||||
.replace(/\u2424/g, '\n')
|
||||
.replace(TAG_RE, (m, name, attr) => {
|
||||
// 标签内的换行, 转为一组特殊字符, 方便后面还原
|
||||
return `<${name + attr.replace(/\n/g, ATTR_BR_SYMBOL)}>`
|
||||
})
|
||||
.replace(BLOCK_RE, (m, tag, attr, txt) => {
|
||||
return `<${tag + attr}>${txt.replace(/\n/g, NODE_BR_SYMBOL)}</${tag}>`
|
||||
})
|
||||
.replace(CODEBLOCK_RE, (m, lang, txt) => {
|
||||
// 还原换行
|
||||
let rollback = txt.replace(NODE_BR_EXP, '\n').replace(ATTR_BR_EXP, '\n')
|
||||
return '```' + lang + rollback + '```'
|
||||
})
|
||||
.replace(BLOCK_RE, (m, tag, attr, txt) => {
|
||||
return `<${tag + attr.replace(ATTR_BR_EXP, ' ')}>${txt
|
||||
.replace(NODE_BR_EXP, '\n')
|
||||
.replace(ATTR_BR_EXP, ' ')}</${tag}>`
|
||||
})
|
||||
}
|
||||
|
||||
class Tool {
|
||||
constructor(list, links) {
|
||||
this.list = list
|
||||
this.__LINKS__ = links
|
||||
}
|
||||
|
||||
// 初始化字符串, 处理多余换行等
|
||||
static init(str) {
|
||||
var links = {}
|
||||
var list = []
|
||||
var lines = str.split('\n')
|
||||
var isCodeBlock = false // 是否代码块
|
||||
var isTable = false // 是否表格
|
||||
var emptyLineLength = 0 //连续空行的数量
|
||||
|
||||
// console.log(lines)
|
||||
|
||||
for (let it of lines) {
|
||||
let tmp = it.trim()
|
||||
|
||||
// 非空行
|
||||
if (tmp) {
|
||||
emptyLineLength = 0
|
||||
if (tmp.startsWith('```')) {
|
||||
if (isCodeBlock) {
|
||||
list.push('</xmp></wc-code>')
|
||||
} else {
|
||||
list.push(
|
||||
tmp.replace(/^```([\w\#\-]*?)$/, `<wc-code lang="$1"><xmp>`)
|
||||
)
|
||||
}
|
||||
isCodeBlock = !isCodeBlock
|
||||
} else {
|
||||
var qlink
|
||||
if (isCodeBlock) {
|
||||
it = it
|
||||
.replace(/<(\/?)([a-z][a-z\d\-]*?)([^>]*?)>/g, '<$1$2$3>')
|
||||
.replace('\\`\\`\\`', '```')
|
||||
} else {
|
||||
if (Helper.isTable(tmp) && !isTable) {
|
||||
var thead = tmp.split('|')
|
||||
// 去头去尾
|
||||
thead.shift()
|
||||
thead.pop()
|
||||
list.push(
|
||||
`<table><thead><tr>${thead
|
||||
.map(_ => `<th>${_}</th>`)
|
||||
.join('')}</tr></thead><tbody>`
|
||||
)
|
||||
isTable = true
|
||||
continue
|
||||
} else {
|
||||
it = it
|
||||
// 非代码块进行xss过滤
|
||||
.replace(INLINE.code, (m, txt) => {
|
||||
return `\`${txt
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')}\``
|
||||
})
|
||||
.replace(/<(\/?)script[^>]*?>/g, '<$1script>')
|
||||
.replace(TAG_RE, (m, name, attr = '') => {
|
||||
// 过滤所有onXX=""事件属性
|
||||
attr = attr.replace(ATTR_RE, ' ').trim()
|
||||
if (attr) {
|
||||
attr = ' ' + attr
|
||||
}
|
||||
return `<${name}${attr}>`
|
||||
})
|
||||
// 不在代码块中, 才判断引用声明
|
||||
qlink = Helper.isQLink(it)
|
||||
}
|
||||
}
|
||||
|
||||
if (qlink) {
|
||||
Object.assign(links, qlink)
|
||||
} else {
|
||||
list.push(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isTable) {
|
||||
isTable = false
|
||||
list.push('</tbody></table>')
|
||||
continue
|
||||
}
|
||||
if (list.length === 0 || (!isCodeBlock && emptyLineLength > 0)) {
|
||||
continue
|
||||
}
|
||||
emptyLineLength++
|
||||
list.push(tmp)
|
||||
}
|
||||
}
|
||||
return new this(list, links)
|
||||
}
|
||||
|
||||
parse() {
|
||||
var html = ''
|
||||
var isCodeBlock = false // 是否代码块
|
||||
var emptyLineLength = 0 //连续空行的数量
|
||||
var isBlockquote = false
|
||||
var isTable = false
|
||||
var tableAlign = null
|
||||
var blockquoteLevel = 0
|
||||
var isParagraph = false
|
||||
|
||||
var isList = false
|
||||
var orderListLevel = -1
|
||||
var unorderListLevel = -1
|
||||
|
||||
var isQuoteList = false // 引用中的列表, 只支持一层级
|
||||
var quoteListStyle = 0 // 1有序, 2 无序
|
||||
|
||||
//
|
||||
for (let it of this.list) {
|
||||
// 非空行
|
||||
if (it) {
|
||||
emptyLineLength = 0
|
||||
|
||||
if (~it.indexOf('<table>') || ~it.indexOf('</table>')) {
|
||||
html += it
|
||||
isTable = !isTable
|
||||
tableAlign = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (isTable) {
|
||||
let tmp = it.split('|').map(_ => _.trim())
|
||||
tmp.shift()
|
||||
tmp.pop()
|
||||
|
||||
// 表格分割行, 配置对齐方式的
|
||||
if (tableAlign === true) {
|
||||
tableAlign = tmp.map(a => {
|
||||
a = a.split(/\-+/)
|
||||
if (a[0] === ':' && a[1] === ':') {
|
||||
return 'align="center"'
|
||||
}
|
||||
if (a[1] === ':') {
|
||||
return 'align="right"'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
continue
|
||||
}
|
||||
html += `<tr>${tmp
|
||||
.map(
|
||||
(_, i) =>
|
||||
`<td ${tableAlign[i]}>${Decoder.inline.call(this, _)}</td>`
|
||||
)
|
||||
.join('')}</tr>`
|
||||
continue
|
||||
}
|
||||
|
||||
// wc-code标签直接拼接, 判断时多拼一个 < 和 >,
|
||||
// 是为了避免在 wc-markd嵌入代码块示例时, 将其内容编译为html
|
||||
if (~it.indexOf('<wc-code') || ~it.indexOf('wc-code>')) {
|
||||
if (isParagraph) {
|
||||
isParagraph = false
|
||||
html += '</p>'
|
||||
}
|
||||
html += it
|
||||
isCodeBlock = !isCodeBlock
|
||||
continue
|
||||
}
|
||||
|
||||
// 同上代码块的处理
|
||||
if (isCodeBlock) {
|
||||
html += '\n' + it
|
||||
continue
|
||||
}
|
||||
|
||||
// 无属性标签
|
||||
|
||||
let hrName = Helper.isHr(it)
|
||||
if (typeof hrName === 'string') {
|
||||
html += Decoder.hr(hrName)
|
||||
continue
|
||||
}
|
||||
|
||||
// 优先处理一些常规样式
|
||||
it = Decoder.inline.call(this, it)
|
||||
|
||||
// 标题只能是单行
|
||||
|
||||
let head = Decoder.head(it)
|
||||
if (head) {
|
||||
isParagraph = false
|
||||
html += head
|
||||
// console.log(html)
|
||||
continue
|
||||
}
|
||||
|
||||
// 引用
|
||||
if (it.startsWith('>')) {
|
||||
let innerQuote // 是否有缩进引用
|
||||
it = it.replace(/^(>+) /, (p, m) => {
|
||||
let len = m.length
|
||||
let tmp = ''
|
||||
let loop = len
|
||||
// 若之前已经有一个未闭合的引用, 需要减去已有缩进级别, 避免产生新的引用标签
|
||||
if (isBlockquote) {
|
||||
loop = len - blockquoteLevel
|
||||
} else {
|
||||
}
|
||||
|
||||
while (loop > 0) {
|
||||
loop--
|
||||
tmp += '<blockquote class="md-quote">'
|
||||
}
|
||||
|
||||
blockquoteLevel = len
|
||||
innerQuote = !!tmp
|
||||
return tmp
|
||||
})
|
||||
|
||||
if (isBlockquote) {
|
||||
// 没有新的缩进引用时, 才添加换行
|
||||
if (innerQuote) {
|
||||
// 之前有引用的列表时, 直接结束列表
|
||||
if (isQuoteList) {
|
||||
html += `</${quoteListStyle === 1 ? 'ul' : 'ul'}>`
|
||||
isQuoteList = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let qListChecked = it.match(INLINE.qlist)
|
||||
if (qListChecked) {
|
||||
let tmp1 = qListChecked[1] // 缩进的标签
|
||||
let tmp2 = +qListChecked[3] // 有序还是无序
|
||||
let tmp3 = qListChecked.pop() // 文本
|
||||
let currListStyle = tmp2 === tmp2 ? 1 : 2
|
||||
var qlist = ''
|
||||
|
||||
// 已有列表
|
||||
if (isQuoteList) {
|
||||
// 因为只支持一层级的列表, 所以同一级别不区分有序无序, 强制统一
|
||||
} else {
|
||||
isQuoteList = true
|
||||
if (currListStyle === 1) {
|
||||
qlist += '<ol>'
|
||||
} else {
|
||||
qlist += '<ul>'
|
||||
}
|
||||
}
|
||||
|
||||
quoteListStyle = currListStyle
|
||||
|
||||
qlist += `<li>${tmp3}</li>`
|
||||
html += tmp1 + qlist
|
||||
} else {
|
||||
if (innerQuote === false) {
|
||||
html += '<br>'
|
||||
}
|
||||
html += it
|
||||
}
|
||||
|
||||
isParagraph = false
|
||||
isBlockquote = true
|
||||
continue
|
||||
}
|
||||
|
||||
// 任务
|
||||
let task = Decoder.task(it)
|
||||
if (task) {
|
||||
html += task
|
||||
continue
|
||||
}
|
||||
|
||||
// 列表
|
||||
let listChecked = Helper.isList(it)
|
||||
if (~listChecked) {
|
||||
// 左侧空格长度
|
||||
let tmp = Helper.ltrim(it)
|
||||
let ltrim = it.length - tmp.length
|
||||
let word = tmp.replace(LIST_RE, '').trim()
|
||||
let level = Math.floor(ltrim / 2)
|
||||
let tag = listChecked > 0 ? 'ol' : 'ul'
|
||||
|
||||
if (isList) {
|
||||
if (listChecked === 1) {
|
||||
if (level > orderListLevel) {
|
||||
html = html.replace(/<\/li>$/, '')
|
||||
html += `<${tag}><li>${word}</li>`
|
||||
} else if (level === orderListLevel) {
|
||||
html += `<li>${word}</li>`
|
||||
} else {
|
||||
html += `</${tag}></li><li>${word}</li>`
|
||||
}
|
||||
orderListLevel = level
|
||||
} else {
|
||||
if (level > unorderListLevel) {
|
||||
html = html.replace(/<\/li>$/, '')
|
||||
html += `<${tag}><li>${word}</li>`
|
||||
} else if (level === unorderListLevel) {
|
||||
html += `<li>${word}</li>`
|
||||
} else {
|
||||
html += `</${tag}></li><li>${word}</li>`
|
||||
}
|
||||
unorderListLevel = level
|
||||
}
|
||||
} else {
|
||||
html += `<${tag}>`
|
||||
if (listChecked === 1) {
|
||||
orderListLevel = level
|
||||
} else {
|
||||
unorderListLevel = level
|
||||
}
|
||||
html += `<li>${word}</li>`
|
||||
}
|
||||
|
||||
isList = true
|
||||
continue
|
||||
}
|
||||
|
||||
// 无"> "前缀的引用, 继续拼到之前的, 并且不换行
|
||||
if (isBlockquote) {
|
||||
html += it
|
||||
continue
|
||||
}
|
||||
|
||||
if (Helper.isNativeDom(it)) {
|
||||
html += it
|
||||
continue
|
||||
}
|
||||
|
||||
if (isParagraph) {
|
||||
html += `${it}<br>`
|
||||
} else {
|
||||
html += `<p>${it}<br>`
|
||||
}
|
||||
isParagraph = true
|
||||
} else {
|
||||
// 如果是在代码中, 直接拼接, 并加上换行
|
||||
if (isCodeBlock) {
|
||||
html += it + '\n'
|
||||
} else {
|
||||
emptyLineLength++
|
||||
|
||||
// 引用结束
|
||||
if (isBlockquote) {
|
||||
isBlockquote = false
|
||||
if (emptyLineLength > 1) {
|
||||
emptyLineLength = 0
|
||||
while (blockquoteLevel > 0) {
|
||||
blockquoteLevel--
|
||||
html += '</blockquote>'
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (isList) {
|
||||
console.log('isList: ', emptyLineLength)
|
||||
if (emptyLineLength > 1) {
|
||||
while (orderListLevel > -1 || unorderListLevel > -1) {
|
||||
if (orderListLevel > unorderListLevel) {
|
||||
html += '</ol>'
|
||||
orderListLevel--
|
||||
} else {
|
||||
html += '</ul>'
|
||||
unorderListLevel--
|
||||
}
|
||||
}
|
||||
isList = false
|
||||
emptyLineLength = 0
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
//
|
||||
if (isParagraph) {
|
||||
if (emptyLineLength > 1) {
|
||||
isParagraph = false
|
||||
html += '</p>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修正内嵌样式
|
||||
html = html.replace(STYLE_RE, (m, code) => {
|
||||
return `<style>${code
|
||||
.replace(/<br>/g, '')
|
||||
.replace(/<p>/g, '')
|
||||
.replace(/<\/p>/g, '')}</style>`
|
||||
})
|
||||
delete this.list
|
||||
delete this.__LINKS__
|
||||
return html
|
||||
}
|
||||
}
|
||||
|
||||
export default function (str) {
|
||||
return Tool.init(fixed(str)).parse()
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/**
|
||||
* {}
|
||||
* @author yutent<yutent.io@gmail.com>
|
||||
* @date 2023/03/20 18:02:01
|
||||
*/
|
||||
import { html, css, Component, nextTick } from '@bd/core'
|
||||
|
||||
class Code extends Component {
|
||||
static props = {
|
||||
code: { type: String, default: '', attribute: false },
|
||||
lang: ''
|
||||
}
|
||||
|
||||
static styles = [
|
||||
css`
|
||||
:host {
|
||||
display: flex;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.code-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
border-radius: 3px;
|
||||
background: #f7f8fb;
|
||||
color: var(--color-dark-1);
|
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
line-height: 1;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.title section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title i {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-red-1);
|
||||
}
|
||||
|
||||
.title i:nth-child(2) {
|
||||
background: var(--color-orange-1);
|
||||
}
|
||||
.title i:nth-child(3) {
|
||||
background: var(--color-green-1);
|
||||
}
|
||||
`,
|
||||
css`
|
||||
.code-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--color-dark-1);
|
||||
cursor: text;
|
||||
counter-reset: code;
|
||||
}
|
||||
.code-block code {
|
||||
display: block;
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
padding: 0 8px 0 45px;
|
||||
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.code-block code::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 40px;
|
||||
height: 100%;
|
||||
padding-right: 5px;
|
||||
text-align: right;
|
||||
color: var(--color-grey-1);
|
||||
content: counter(code);
|
||||
counter-increment: code;
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
mounted() {
|
||||
var txt = this.innerHTML || this.textContent
|
||||
txt = txt.trim().replace(/^[\r\n]|\s{2,}$/g, '')
|
||||
if (txt.startsWith('<xmp>') && txt.endsWith('</xmp>')) {
|
||||
txt = txt.slice(5, -6).trim()
|
||||
}
|
||||
if (txt) {
|
||||
this.textContent = ''
|
||||
nextTick(_ => {
|
||||
this.code = txt.replace(/</g, '<').replace(/>/g, '>')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="code-box">
|
||||
<header class="title">
|
||||
<section><i></i><i></i><i></i></section>
|
||||
<section>${this.lang}</section>
|
||||
</header>
|
||||
<div class="code-block">
|
||||
${this.code.split('\n').map(s => html`<code>${s}</code>`)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
Code.reg('code')
|
|
@ -1,11 +1,8 @@
|
|||
|
||||
import { createApp } from 'vue'
|
||||
import App from './app.vue'
|
||||
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router).use(store).mount('.app')
|
||||
|
||||
app.use(store).mount('.app')
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from './views/home.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('./views/about.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
|
12
src/store.js
12
src/store.js
|
@ -1,9 +1,15 @@
|
|||
|
||||
import { reactive } from 'vue'
|
||||
|
||||
const store = reactive({
|
||||
foo: 'bar',
|
||||
version: '0.4.0'
|
||||
API_KEY: localStorage.getItem('API_KEY'),
|
||||
BASE_URL: localStorage.getItem('BASE_URL'),
|
||||
|
||||
conversations: [],
|
||||
conversation: {
|
||||
id: '',
|
||||
lastMessageId: ''
|
||||
},
|
||||
records: []
|
||||
})
|
||||
|
||||
export default function (app) {
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
content: '这是关于我们页面'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>{{content}}</h1>
|
||||
<cite>当前vue-live版本: v{{$store.version}}</cite>
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
<template>
|
||||
<aside class="history noselect">
|
||||
<el-button type="warning" plain class="new" @click="createNewConversation"
|
||||
>+ 新建会话</el-button
|
||||
>
|
||||
<ul class="list">
|
||||
<li
|
||||
class="item"
|
||||
v-for="(it, i) in $store.conversations"
|
||||
:key="it.id"
|
||||
@click="pickThisConversation(it)"
|
||||
:class="{ active: it.id === $store.conversation.id }"
|
||||
>
|
||||
<span class="text-ell">{{ it.name }}</span>
|
||||
<a class="close" @click.stop="removeConversation(it, i)"> ╳ </a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
conversations: []
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
createNewConversation() {
|
||||
this.conversation = {
|
||||
id: '',
|
||||
lastMessageId: ''
|
||||
}
|
||||
this.records = []
|
||||
},
|
||||
|
||||
removeConversation(it, idx) {
|
||||
this.$confirm(`是否删除此会话【${it.name}】`)
|
||||
.then(_ => {
|
||||
removeConversation(it.id)
|
||||
.then(r => {
|
||||
this.$message.success('删除会话成功')
|
||||
this.conversations.splice(idx, 1)
|
||||
if (this.conversations.length) {
|
||||
this.pickThisConversation(this.conversations[0])
|
||||
} else {
|
||||
this.createNewConversation()
|
||||
}
|
||||
})
|
||||
.catch(r => {
|
||||
this.$message.success('删除会话失败')
|
||||
})
|
||||
})
|
||||
.catch(function () {})
|
||||
},
|
||||
|
||||
pickThisConversation(it) {
|
||||
if (it.id === this.conversation.id) {
|
||||
return
|
||||
}
|
||||
this.conversation = { id: it.id }
|
||||
this.records = []
|
||||
this.loading = true
|
||||
this.getRecords()
|
||||
.then(list => {
|
||||
this.conversation.lastMessageId = list.at(-1).id
|
||||
nextTick(_ => {
|
||||
this.$refs.records.scrollTop = Number.MAX_SAFE_INTEGER
|
||||
this.$refs.input.focus()
|
||||
})
|
||||
})
|
||||
.finally(_ => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.history {
|
||||
flex-shrink: 0;
|
||||
width: 220px;
|
||||
padding: 8px 16px;
|
||||
border-right: 1px solid var(--color-plain-2);
|
||||
|
||||
.new {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin-top: 8px;
|
||||
padding: 0 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--color-dark-1);
|
||||
transition: color 0.2s ease-in, background 0.2s ease-in;
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.close {
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.2s ease-in;
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:hover {
|
||||
color: var(--color-blue-1);
|
||||
background: #ecf5ff;
|
||||
|
||||
.close {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,280 @@
|
|||
<template>
|
||||
<div class="current-session">
|
||||
<div class="records" ref="records">
|
||||
<section class="item" v-for="it in records" :key="it.id">
|
||||
<Avatar :name="it.role === 0 ? '我' : ''" />
|
||||
<div class="content" v-html="markd(it.content)"></div>
|
||||
</section>
|
||||
</div>
|
||||
<textarea
|
||||
ref="input"
|
||||
class="question"
|
||||
autofocus
|
||||
:disabled="loading"
|
||||
v-model="question"
|
||||
@keydown="ask"
|
||||
:placeholder="'Ctrl/Shift/Cmd + 回车换行 \n回车发送'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from '@/components/avatar.vue'
|
||||
import '@/lib/wc-code.js'
|
||||
import { nextTick } from 'vue'
|
||||
import markd from '@/lib/markd.js'
|
||||
|
||||
function ask() {
|
||||
//
|
||||
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
conversation: '',
|
||||
id: '',
|
||||
text: 'blabla...'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function get10Tokens(str = '') {
|
||||
return str.slice(0, 10)
|
||||
}
|
||||
|
||||
export default {
|
||||
components: { Avatar },
|
||||
data() {
|
||||
return {
|
||||
records: [],
|
||||
question: '',
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// this.getConversations()
|
||||
// this.getRecords()
|
||||
|
||||
nextTick(_ => this.$refs.input.focus())
|
||||
},
|
||||
|
||||
methods: {
|
||||
markd,
|
||||
|
||||
ask(ev) {
|
||||
let question = this.question.trim()
|
||||
let { id, lastMessageId } = this.$store.conversation
|
||||
|
||||
if (ev.keyCode === 13) {
|
||||
if (ev.shiftKey) {
|
||||
return
|
||||
}
|
||||
if (ev.ctrlKey || ev.metaKey) {
|
||||
this.question += '\n'
|
||||
return
|
||||
}
|
||||
|
||||
ev.preventDefault()
|
||||
|
||||
if (question === '') {
|
||||
return
|
||||
}
|
||||
|
||||
this.question = ''
|
||||
|
||||
this.records.push({ id: Date.now(), role: 0, content: question })
|
||||
|
||||
nextTick(_ => (this.$refs.records.scrollTop = Number.MAX_SAFE_INTEGER))
|
||||
|
||||
this.loading = true
|
||||
|
||||
this.records.push({
|
||||
id: Date.now(),
|
||||
role: 1,
|
||||
content: '<div class="loading"><i></i><i></i><i></i></div>'
|
||||
})
|
||||
|
||||
ask(question, lastMessageId, id)
|
||||
.then(r => {
|
||||
if (!id) {
|
||||
this.$store.conversations.unshift({
|
||||
id: r.data.conversation,
|
||||
name: get10Tokens(question)
|
||||
})
|
||||
}
|
||||
this.$store.conversation.id = r.data.conversation
|
||||
this.$store.conversation.lastMessageId = r.data.id
|
||||
|
||||
this.records.at(-1).id = r.data.id
|
||||
this.records.at(-1).content = r.data.text
|
||||
})
|
||||
.catch(r => {
|
||||
console.log(r)
|
||||
this.records.at(-1).content = r.msg || r.toString()
|
||||
|
||||
this.$message.error(r.msg || r.toString())
|
||||
})
|
||||
.finally(_ => {
|
||||
this.loading = false
|
||||
nextTick(_ => {
|
||||
this.$refs.records.scrollTop = Number.MAX_SAFE_INTEGER
|
||||
this.$refs.input.focus()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.current-session {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
background: var(--color-plain-1);
|
||||
|
||||
.records {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--color-dark-1);
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
margin-left: 16px;
|
||||
padding: 8px 16px;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 10px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
transform: rotate(45deg);
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(odd) {
|
||||
.content {
|
||||
background: #d9ecff;
|
||||
&::before {
|
||||
background: #d9ecff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.question {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
height: 128px;
|
||||
margin-top: 32px;
|
||||
padding: 8px;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--color-plain-2);
|
||||
background: #fff;
|
||||
color: var(--color-dark-2);
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 5px var(--color-blue-a);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-grey-2);
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.current-session .content {
|
||||
line-height: 1.5;
|
||||
ol {
|
||||
margin-left: 1em;
|
||||
list-style: decimal outside none;
|
||||
}
|
||||
ul {
|
||||
margin-left: 1em;
|
||||
list-style: disc outside none;
|
||||
}
|
||||
li {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
li ol {
|
||||
margin-left: 1em;
|
||||
}
|
||||
li ul {
|
||||
margin-left: 1em;
|
||||
list-style-type: circle;
|
||||
}
|
||||
li ol ul,
|
||||
li ul ul {
|
||||
list-style-type: square;
|
||||
}
|
||||
|
||||
code.inline {
|
||||
display: inline;
|
||||
margin: 0 2px;
|
||||
padding: 0 2px;
|
||||
color: var(--color-red-1);
|
||||
background: var(--color-plain-1);
|
||||
border-radius: 2px;
|
||||
font-family: Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
}
|
||||
.current-session .loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
|
||||
i {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin: 0 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-blue-1);
|
||||
transform: scale(0.5);
|
||||
animation: loading 1s ease-in-out infinite;
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes loading {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.5);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,17 +0,0 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
content: '欢迎访问~~ 这是首页'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<h1>{{content}}</h1>
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<main class="noselect topbar">
|
||||
<section class="btns"><i></i><i></i><i></i></section>
|
||||
<div class="option">•••</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
content: '这是关于我们页面'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--color-plain-2);
|
||||
}
|
||||
.btns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
i {
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-red-1);
|
||||
&:nth-child(2) {
|
||||
background: var(--color-orange-1);
|
||||
}
|
||||
&:nth-child(3) {
|
||||
background: var(--color-green-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
.option {
|
||||
font-size: 22px;
|
||||
}
|
||||
</style>
|
|
@ -18,7 +18,7 @@ export default {
|
|||
// 这里的resolve可将相对路径转为绝对路径
|
||||
// 如果传入的路径已经是绝对路径的, 可不需要resolve
|
||||
entry: resolve('./src/main.js'),
|
||||
title: 'vue-live 应用示例'
|
||||
title: 'chatgpt网页客户端'
|
||||
}
|
||||
},
|
||||
// 以下cdn地址, 可自行修改为适合的
|
||||
|
@ -26,11 +26,7 @@ export default {
|
|||
// 也可以在页面中直接引入完整的路径, 而不必须在这里声明
|
||||
imports: {
|
||||
vue: '//jscdn.ink/vue/3.2.47/vue.runtime.esm-browser.prod.js',
|
||||
// 这个vue-router库, 移除了 @vue/devtools-api 相关的代码。 以达到减少不必须的体积的效果
|
||||
// 如需要支持devtools的, 请修改为原版vue-router地址即可。
|
||||
// 'vue-router': '//jscdn.ink/@bytedo/vue-router/4.1.6/vue-router.js',
|
||||
// 'vue-router': '//jscdn.ink/vue-router/4.1.6/vue-router.esm-browser.js',
|
||||
// '@vue/devtools-api': '//jscdn.ink/@vue/devtools-api/6.5.0/esm/index.js',
|
||||
'@bd/core.js': '//jscdn.ink/@bd/core/1.6.0/index.js',
|
||||
fetch: '//jscdn.ink/@bytedo/fetch/2.1.5/next.js'
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue