v1.0.0
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
<script setup lang='ts'>
|
||||
import { defineAsyncComponent, ref } from 'vue'
|
||||
import { HoverButton, SvgIcon } from '@/components/common'
|
||||
|
||||
const Setting = defineAsyncComponent(() => import('@/components/common/Setting/index.vue'))
|
||||
|
||||
const show = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t dark:border-neutral-800">
|
||||
<div class="flex-1 flex-shrink-0 overflow-hidden">
|
||||
<!-- <UserAvatar /> -->
|
||||
</div>
|
||||
|
||||
<HoverButton @click="show = true">
|
||||
<span class="text-xl text-[#4f555e] dark:text-white">
|
||||
<SvgIcon icon="ri:settings-4-line" />
|
||||
</span>
|
||||
</HoverButton>
|
||||
|
||||
<Setting v-if="show" v-model:visible="show" />
|
||||
</footer>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
import UserInfoFooter from './UserInfoFooter/index.vue'
|
||||
|
||||
export { UserInfoFooter }
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { NButton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goHome() {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full">
|
||||
<div class="px-4 m-auto space-y-4 text-center max-[400px]">
|
||||
<h1 class="text-4xl text-slate-800 dark:text-neutral-200">
|
||||
<!-- Sorry, page not found! -->
|
||||
页面不存在
|
||||
</h1>
|
||||
<p class="text-base text-slate-500 dark:text-neutral-400">
|
||||
<!-- Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling. -->
|
||||
</p>
|
||||
<div class="flex items-center justify-center text-center">
|
||||
<div class="w-[300px]">
|
||||
<img src="../../../icons/404.svg" alt="404">
|
||||
</div>
|
||||
</div>
|
||||
<NButton type="primary" @click="goHome">
|
||||
<!-- Go to Home -->
|
||||
返回首页
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts" setup>
|
||||
import { NButton } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon500 from '@/icons/500.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goHome() {
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full dark:bg-neutral-800">
|
||||
<div class="px-4 m-auto space-y-4 text-center max-[400px]">
|
||||
<header class="space-y-2">
|
||||
<h2 class="text-2xl font-bold text-center text-slate-800 dark:text-neutral-200">
|
||||
500
|
||||
</h2>
|
||||
<p class="text-base text-center text-slate-500 dark:text-slate-500">
|
||||
<!-- Server error -->
|
||||
服务器错误
|
||||
</p>
|
||||
<div class="flex items-center justify-center text-center">
|
||||
<Icon500 class="w-[300px]" />
|
||||
</div>
|
||||
</header>
|
||||
<NButton type="primary" @click="goHome">
|
||||
<!-- Go to Home -->
|
||||
返回首页
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div v-for="(item, index) in items" :key="index" :class="isChilden ? 'ml-[20px]' : ''">
|
||||
<!-- <span class="items-center flex">
|
||||
<NAvatar round :size="30" :src="item.headImage ? item.headImage : defaultAvatar" />
|
||||
</span> -->
|
||||
|
||||
|
||||
<div
|
||||
class="flex items-center cursor-pointer my-[15px] p-[5px] hover:bg-slate-100 dark:hover:bg-slate-800 rounded-md">
|
||||
|
||||
<div class="w-[25px]">
|
||||
<span v-if="item.children" class="cursor-pointer">
|
||||
<!-- 收起展开:'rotate-90' :'rotate-0' -->
|
||||
<SvgIcon width="18" height="18" class="list-expand" :class="item.extand?'rotate-90':'rotate-0'" icon="ic:round-play-arrow" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="flex items-center mr-[5px] text-slate-400 hover:text-black dark:hover:text-white">
|
||||
{{ item.name }}
|
||||
<!-- <NDropdown trigger="click" :options="item.isTop === 1 ? unSetTopOptions : setTopOptions" size="small"
|
||||
@select="(key: string) => handleClick(key, item.id)">
|
||||
<SvgIcon icon="ri:more-fill" />
|
||||
</NDropdown> -->
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
</div>
|
||||
<div v-show="item.extand">
|
||||
<!-- 如果有子级,递归渲染 -->
|
||||
<recursive-list v-if="item.children" :is-childen="true" :items="item.children"></recursive-list>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { defineProps } from "vue"
|
||||
defineProps<{
|
||||
items: Array<any>,
|
||||
isChilden?: boolean
|
||||
}>()
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.list-expand {
|
||||
transition-property: transform;
|
||||
transition-duration: .25s;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup lang='ts'>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
html,body{
|
||||
padding: 20px;
|
||||
background-color: beige;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import RecursiveList from './RecursiveList.vue'
|
||||
|
||||
const multiLevelData = [
|
||||
{
|
||||
name: '开发笔记',
|
||||
children: [
|
||||
{
|
||||
name: 'Level 2 Item 1',
|
||||
extand: true,
|
||||
children: [
|
||||
{ name: 'SAI-Chat开发' },
|
||||
{ name: '笔记本项目' },
|
||||
],
|
||||
},
|
||||
{ name: '测试项目' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '学习笔记',
|
||||
children: [
|
||||
{ name: 'Blender' },
|
||||
{ name: 'mongo' },
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<RecursiveList :items="multiLevelData" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang='ts'>
|
||||
import { NLayout, NLayoutContent, NLayoutSider } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
// import { Header } from './components'
|
||||
// import { LeftSider, RightSider } from './layout'
|
||||
import Index from './index.vue'
|
||||
import { usePanelState } from '@/store/modules'
|
||||
|
||||
const panelState = usePanelState()
|
||||
|
||||
const leftSiderCollapsed = computed(() => panelState.leftSiderCollapsed)
|
||||
const rightSiderCollapsed = computed(() => panelState.rightSiderCollapsed)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full">
|
||||
<NLayout has-sider class="h-full">
|
||||
<NLayoutSider
|
||||
v-model:collapsed="leftSiderCollapsed"
|
||||
collapse-mode="transform"
|
||||
:collapsed-width="0"
|
||||
:width="240"
|
||||
bordered
|
||||
>
|
||||
<LeftSider />
|
||||
</NLayoutSider>
|
||||
<NLayoutContent>
|
||||
<NLayout has-sider sider-placement="right" class="h-full">
|
||||
<NLayoutContent class="h-full">
|
||||
<!-- 内容 -->
|
||||
<!-- <Header /> -->
|
||||
<div>
|
||||
<Index />
|
||||
</div>
|
||||
</NLayoutContent>
|
||||
<NLayoutSider
|
||||
v-model:collapsed="rightSiderCollapsed"
|
||||
collapse-mode="transform"
|
||||
:collapsed-width="0"
|
||||
:width="280"
|
||||
content-style="padding: 20px;"
|
||||
bordered
|
||||
>
|
||||
<RightSider />
|
||||
</NLayoutSider>
|
||||
</NLayout>
|
||||
</NLayoutContent>
|
||||
</NLayout>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,152 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NColorPicker, NInput, NRadio, NUpload, useMessage } from 'naive-ui'
|
||||
import type { UploadFileInfo } from 'naive-ui'
|
||||
import { defineProps, ref } from 'vue'
|
||||
import { ItemIcon } from '@/components/common'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
const props = defineProps<{
|
||||
itemIcon: Panel.ItemIcon | null
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:itemIcon', visible: Panel.ItemIcon): void // 定义修改父组件(prop内)的值的事件
|
||||
}>()
|
||||
const authStore = useAuthStore()
|
||||
const ms = useMessage()
|
||||
const checkedValueRef = ref<number | null>(props.itemIcon?.itemType || 1)
|
||||
|
||||
// 默认图标背景色
|
||||
const defautSwatchesBackground = [
|
||||
'#000',
|
||||
'#18A058',
|
||||
'#2080F0',
|
||||
'#F0A020',
|
||||
'rgba(208, 48, 80, 1)',
|
||||
]
|
||||
|
||||
const initData: Panel.ItemIcon = {
|
||||
itemType: 1,
|
||||
bgColor: '#000',
|
||||
}
|
||||
|
||||
const itemIconInfo = ref<Panel.ItemIcon>(props.itemIcon ? { ...props.itemIcon } : { ...initData })
|
||||
|
||||
function handleIconTypeRadioChange(type: number) {
|
||||
checkedValueRef.value = type
|
||||
itemIconInfo.value.itemType = type
|
||||
emit('update:itemIcon', itemIconInfo.value)
|
||||
}
|
||||
|
||||
function handleChange() {
|
||||
// console.log('值', itemIconInfo.value)
|
||||
emit('update:itemIcon', itemIconInfo.value || null)
|
||||
}
|
||||
|
||||
const handleUploadFinish = ({
|
||||
file,
|
||||
event,
|
||||
}: {
|
||||
file: UploadFileInfo
|
||||
event?: ProgressEvent
|
||||
}) => {
|
||||
const res = JSON.parse((event?.target as XMLHttpRequest).response)
|
||||
if (res.code === 0) {
|
||||
const imageUrl = res.data.imageUrl
|
||||
itemIconInfo.value.src = imageUrl
|
||||
emit('update:itemIcon', itemIconInfo.value || null)
|
||||
}
|
||||
else {
|
||||
ms.error(`上传错误:${res.msg}`)
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-[10px]">
|
||||
<NRadio
|
||||
:checked="checkedValueRef === 1 "
|
||||
:value="1"
|
||||
name="iconType"
|
||||
@change="handleIconTypeRadioChange(1)"
|
||||
>
|
||||
文字
|
||||
</NRadio>
|
||||
|
||||
<NRadio
|
||||
:checked="checkedValueRef === 2"
|
||||
:value="2"
|
||||
name="iconType"
|
||||
@change="handleIconTypeRadioChange(2)"
|
||||
>
|
||||
图片/SVG
|
||||
</NRadio>
|
||||
|
||||
<NRadio
|
||||
:checked="checkedValueRef === 3"
|
||||
:value="3"
|
||||
name="iconType"
|
||||
@change="handleIconTypeRadioChange(3)"
|
||||
>
|
||||
图标
|
||||
</NRadio>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[100px]">
|
||||
<div>
|
||||
<div class="border rounded-2xl bg-slate-200">
|
||||
<ItemIcon :item-icon="itemIconInfo" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文字 -->
|
||||
<div class="ml-[20px]">
|
||||
<!-- <NImage :src="model.icon" preview-disabled /> -->
|
||||
<div v-if="checkedValueRef === 1">
|
||||
<NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入文字作为图标" @input="handleChange" />
|
||||
<NColorPicker
|
||||
v-model:value="itemIconInfo.bgColor"
|
||||
size="small"
|
||||
:modes="['hex']"
|
||||
:swatches="defautSwatchesBackground"
|
||||
@complete="handleChange"
|
||||
@update-value="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="checkedValueRef === 3">
|
||||
<div>
|
||||
<NInput v-model:value="itemIconInfo.text" class="mb-[5px]" size="small" type="text" placeholder="请输入图标名字" @input="handleChange" />
|
||||
<a target="_blank" href="https://icon-sets.iconify.design/" class="text-[blue]">图标列表</a>
|
||||
</div>
|
||||
<NColorPicker
|
||||
v-model:value="itemIconInfo.bgColor"
|
||||
size="small"
|
||||
:modes="['hex']"
|
||||
:swatches="defautSwatchesBackground"
|
||||
@complete="handleChange"
|
||||
@update-value="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 图片 -->
|
||||
<div v-if="checkedValueRef === 2">
|
||||
<NUpload
|
||||
action="/api/file/uploadImg"
|
||||
:show-file-list="false"
|
||||
name="imgfile"
|
||||
:headers="{
|
||||
token: authStore.token as string,
|
||||
}"
|
||||
@finish="handleUploadFinish"
|
||||
>
|
||||
<NButton size="small">
|
||||
点击上传
|
||||
</NButton>
|
||||
</NUpload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, defineProps, onMounted, ref, watch } from 'vue'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
import { NButton, NForm, NFormItem, NInput, NModal, NSelect, useMessage } from 'naive-ui'
|
||||
import IconEditor from './IconEditor.vue'
|
||||
import { edit } from '@/api/panel/itemIcon'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
itemInfo: Panel.Info | null
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emit>()
|
||||
const ms = useMessage()
|
||||
|
||||
const restoreDefault: Panel.Info = {
|
||||
icon: null,
|
||||
title: '',
|
||||
url: '',
|
||||
lanUrl: '',
|
||||
description: '',
|
||||
openMethod: 1,
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'done', item: Panel.Info): void// 创建完成
|
||||
}
|
||||
|
||||
const model = ref<Panel.Info>(props.itemInfo !== null ? { ...props.itemInfo } : { ...restoreDefault })
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
const rules: FormRules = {
|
||||
title: {
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
message: '必填项',
|
||||
},
|
||||
url: {
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
type: 'string',
|
||||
message: '必填项',
|
||||
},
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
default: true,
|
||||
label: '当前页面打开',
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
label: '新窗口打开',
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
label: '弹窗打开',
|
||||
value: 3,
|
||||
},
|
||||
]
|
||||
|
||||
// 更新值父组件传来的值
|
||||
const show = computed({
|
||||
get: () => props.visible,
|
||||
set: (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
},
|
||||
})
|
||||
|
||||
async function editApi() {
|
||||
const { code, data } = await edit<Panel.ItemInfo>(model.value)
|
||||
if (code === 0) {
|
||||
show.value = false
|
||||
model.value = restoreDefault
|
||||
|
||||
emit('done', data)
|
||||
}
|
||||
else {
|
||||
ms.error('保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleValidateButtonClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors)
|
||||
editApi()
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.itemInfo, (newValue) => {
|
||||
model.value = newValue || restoreDefault
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// rolesLoading.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" preset="card" style="width: 600px;border-radius: 1rem;" :title="itemInfo ? '修改项目' : '添加项目'">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem path="title" label="标题">
|
||||
<NInput v-model:value="model.title" type="text" show-count :maxlength="20" placeholder="请输入标题" />
|
||||
</NFormItem>
|
||||
<NFormItem path="icon" label="图标">
|
||||
<IconEditor v-model:item-icon="model.icon" />
|
||||
</NFormItem>
|
||||
<NFormItem path="url" label="跳转地址">
|
||||
<NInput v-model:value="model.url" type="text" :maxlength="1000" placeholder="请输入跳转地址" />
|
||||
</NFormItem>
|
||||
<NFormItem path="lanUrl" label="局域网跳转地址">
|
||||
<NInput v-model:value="model.lanUrl" type="text" :maxlength="1000" placeholder="(可以留空)切换到局域网模式,点击会使用该地址" />
|
||||
</NFormItem>
|
||||
<NFormItem path="description" label="描述信息">
|
||||
<NInput v-model:value="model.description" type="text" show-count :maxlength="100" placeholder="请填写描述信息" />
|
||||
</NFormItem>
|
||||
<NFormItem path="openMethod" label="打开方式">
|
||||
<NSelect v-model:value="model.openMethod" :options="options" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<template #footer>
|
||||
<NButton type="success" @click="handleValidateButtonClick">
|
||||
确定
|
||||
</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mdKatex from '@traptitech/markdown-it-katex'
|
||||
import mila from 'markdown-it-link-attributes'
|
||||
import hljs from 'highlight.js'
|
||||
import { NAlert } from 'naive-ui'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
|
||||
interface Props {
|
||||
error?: boolean
|
||||
text?: string
|
||||
loading?: boolean
|
||||
asRawText?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const textRef = ref<HTMLElement>()
|
||||
|
||||
const mdi = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language))
|
||||
if (validLang) {
|
||||
const lang = language ?? ''
|
||||
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
|
||||
}
|
||||
return highlightBlock(hljs.highlightAuto(code).value, '')
|
||||
},
|
||||
})
|
||||
|
||||
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
|
||||
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
return [
|
||||
'text-wrap',
|
||||
'min-w-[20px]',
|
||||
'rounded-md',
|
||||
isMobile.value ? 'p-2' : 'px-3 py-2',
|
||||
'bg-[#f4f6f8]',
|
||||
'dark:bg-[#1e1e20]',
|
||||
'message-reply',
|
||||
{ 'text-red-500': props.error },
|
||||
]
|
||||
})
|
||||
|
||||
function highlightBlock(str: string, lang?: string) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
||||
}
|
||||
|
||||
defineExpose({ textRef })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-black" :class="wrapClass">
|
||||
<div ref="textRef" class="leading-relaxed break-words">
|
||||
<div v-if="error">
|
||||
<NAlert type="warning">
|
||||
{{ text }}
|
||||
</NAlert>
|
||||
</div>
|
||||
<div v-else-if="!asRawText" class="markdown-body" v-html="mdi.render(text ?? '')" />
|
||||
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
||||
<template v-if="loading">
|
||||
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@import url(./style.less);
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import mdKatex from '@traptitech/markdown-it-katex'
|
||||
import mila from 'markdown-it-link-attributes'
|
||||
import hljs from 'highlight.js'
|
||||
import { NAlert } from 'naive-ui'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
|
||||
interface Props {
|
||||
error?: boolean
|
||||
text?: string
|
||||
loading?: boolean
|
||||
asRawText?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const textRef = ref<HTMLElement>()
|
||||
|
||||
const mdi = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
highlight(code, language) {
|
||||
const validLang = !!(language && hljs.getLanguage(language))
|
||||
if (validLang) {
|
||||
const lang = language ?? ''
|
||||
return highlightBlock(hljs.highlight(code, { language: lang }).value, lang)
|
||||
}
|
||||
return highlightBlock(hljs.highlightAuto(code).value, '')
|
||||
},
|
||||
})
|
||||
|
||||
mdi.use(mila, { attrs: { target: '_blank', rel: 'noopener' } })
|
||||
mdi.use(mdKatex, { blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000' })
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
return [
|
||||
'text-wrap',
|
||||
'min-w-[20px]',
|
||||
'rounded-md',
|
||||
isMobile.value ? 'p-2' : 'px-3 py-2',
|
||||
'bg-[#f4f6f8]',
|
||||
'dark:bg-[#1e1e20]',
|
||||
'message-reply',
|
||||
{ 'text-red-500': props.error },
|
||||
]
|
||||
})
|
||||
|
||||
function highlightBlock(str: string, lang?: string) {
|
||||
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${t('chat.copyCode')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
||||
}
|
||||
|
||||
defineExpose({ textRef })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="text-black" :class="wrapClass">
|
||||
<div ref="textRef" class="leading-relaxed break-words">
|
||||
<div v-if="error">
|
||||
<NAlert type="warning">
|
||||
{{ text }}
|
||||
</NAlert>
|
||||
</div>
|
||||
<div v-else-if="!asRawText" class="markdown-body" v-html="mdi.render(text ?? '')" />
|
||||
<div v-else class="whitespace-pre-wrap" v-text="text" />
|
||||
<template v-if="loading">
|
||||
<span class="dark:text-white w-[4px] h-[20px] block animate-blink" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
@import url(./style.less);
|
||||
</style>
|
||||
@@ -0,0 +1,75 @@
|
||||
.markdown-body {
|
||||
background-color: transparent;
|
||||
font-size: 14px;
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
pre code,
|
||||
pre tt {
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
pre {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
&-wrapper {
|
||||
position: relative;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
color: #b3b3b3;
|
||||
|
||||
&__copy {
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
color: #65a665;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
html.dark {
|
||||
|
||||
.message-reply {
|
||||
.whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
color: var(--n-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.highlight pre,
|
||||
pre {
|
||||
background-color: #282c34;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { NTabPane, NTabs } from 'naive-ui'
|
||||
import Style from './tabs/Style.vue'
|
||||
import About from './tabs/About.vue'
|
||||
import Users from './tabs/Users.vue'
|
||||
import UserInfo from './tabs/UserInfo.vue'
|
||||
import { RoundCardModal } from '@/components/common'
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}>()
|
||||
|
||||
const show = computed({
|
||||
get: () => props.visible,
|
||||
set: (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<RoundCardModal v-model:show="show" title="设置" style="max-height: 700px;">
|
||||
<NTabs type="line" animated>
|
||||
<NTabPane name="style" tab="样式">
|
||||
<Style />
|
||||
</NTabPane>
|
||||
<NTabPane name="userInfo" tab="登录信息">
|
||||
<UserInfo />
|
||||
</NTabPane>
|
||||
<NTabPane name="about" tab="关于">
|
||||
<About />
|
||||
</NTabPane>
|
||||
<NTabPane name="password" tab="账号管理">
|
||||
<Users />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</RoundCardModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-shadow{
|
||||
text-shadow: 0px 0px 5px gray;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { NDivider, NGradientText } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { get } from '@/api/system/about'
|
||||
|
||||
interface Version {
|
||||
versionName: string
|
||||
versionCode: number
|
||||
}
|
||||
|
||||
const versionName = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
get<Version>().then((res) => {
|
||||
if (res.code === 0)
|
||||
versionName.value = res.data.versionName
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="text-3xl">
|
||||
<NGradientText type="danger">
|
||||
Sun-Panel
|
||||
</NGradientText>
|
||||
v{{ versionName }}
|
||||
</div>
|
||||
<NDivider />
|
||||
<div class="text-lg">
|
||||
开发者: <a href="https://blog.enianteam.com/u/sun/content/11" target="_blank" class="link">红烧猎人</a>
|
||||
</div>
|
||||
<div class="text-lg">
|
||||
项目开源地址:
|
||||
<a href="https://github.com/hslr-s/sun-panel" target="_blank" class="link">Github</a> |
|
||||
<a href="https://gitee.com/hslr/sun-panel" target="_blank" class="link">Gitee</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.link{
|
||||
color:blue
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, defineEmits, defineProps, ref, watch } from 'vue'
|
||||
import type { FormInst, FormRules } from 'naive-ui'
|
||||
import { NButton, NForm, NFormItem, NInput, useMessage } from 'naive-ui'
|
||||
import { edit as userManageEdit } from '@/api/panel/users'
|
||||
import { RoundCardModal } from '@/components/common'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
userId?: number
|
||||
userInfo?: User.Info
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emit>()
|
||||
const message = useMessage()
|
||||
|
||||
interface Emit {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
(e: 'done', id: number): void// 创建完成
|
||||
}
|
||||
|
||||
const formInitValue = {
|
||||
name: '',
|
||||
username: '',
|
||||
role: 2,
|
||||
status: 3,
|
||||
}
|
||||
|
||||
const model = ref<User.Info>(formInitValue)
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
message: '请输入账号且大于5个字符',
|
||||
min: 5,
|
||||
},
|
||||
{
|
||||
trigger: 'blur',
|
||||
message: '请输入邮箱作为账号',
|
||||
type: 'email',
|
||||
},
|
||||
],
|
||||
role: {
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
type: 'number',
|
||||
message: '请选择角色',
|
||||
},
|
||||
status: {
|
||||
required: true,
|
||||
trigger: 'blur',
|
||||
type: 'number',
|
||||
message: '请选择账号状态',
|
||||
},
|
||||
}
|
||||
|
||||
// 更新值父组件传来的值
|
||||
const show = computed({
|
||||
get: () => props.visible,
|
||||
set: (visible: boolean) => {
|
||||
emit('update:visible', visible)
|
||||
},
|
||||
})
|
||||
|
||||
watch(show, (newValue, oldValue) => {
|
||||
if (props.userInfo?.id)
|
||||
model.value = props.userInfo || {}
|
||||
|
||||
else
|
||||
model.value = formInitValue
|
||||
})
|
||||
|
||||
const add = async () => {
|
||||
const res = await userManageEdit<User.Info>(model.value)
|
||||
if (res.code === 0)
|
||||
emit('done', res.data.id as number)
|
||||
|
||||
else if (res.code !== -1)
|
||||
message.warning('操作失败')
|
||||
}
|
||||
|
||||
const handleValidateButtonClick = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate((errors) => {
|
||||
if (!errors)
|
||||
add()
|
||||
else
|
||||
console.log(errors)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RoundCardModal v-model:show="show" size="small" preset="card" style="width: 400px" :title="`${userInfo?.id ? '编辑' : '添加'}用户`">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem path="username" label="账号">
|
||||
<NInput v-model:value="model.username" type="text" placeholder="邮箱地址作为账号" />
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem path="name" label="昵称">
|
||||
<NInput v-model:value="model.name" type="text" placeholder="请输入昵称" />
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem path="password" label="密码">
|
||||
<NInput v-model:value="model.password" type="text" :placeholder="`${userInfo?.id ? '请输入新密码,留空密码不变' : '请输入密码'}`" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="float-right">
|
||||
<NButton type="success" size="small" @click="handleValidateButtonClick">
|
||||
保存
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</RoundCardModal>
|
||||
</template>
|
||||
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import type { UploadFileInfo } from 'naive-ui'
|
||||
import { NButton, NCard, NColorPicker, NInput, NPopconfirm, NSelect, NSlider, NSwitch, NUpload, NUploadDragger, useMessage } from 'naive-ui'
|
||||
import { useAuthStore, usePanelState } from '@/store'
|
||||
import { set as setUserConfig } from '@/api/panel/userConfig'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const panelState = usePanelState()
|
||||
const ms = useMessage()
|
||||
|
||||
const isSaveing = ref(false)
|
||||
|
||||
const iconTypeOptions = [
|
||||
{
|
||||
label: '详情图标',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
label: '小图标',
|
||||
value: 1,
|
||||
},
|
||||
]
|
||||
|
||||
watch(panelState.panelConfig, () => {
|
||||
if (!isSaveing.value) {
|
||||
isSaveing.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
panelState.recordState()// 本地记录
|
||||
setUserConfig({ panel: panelState.panelConfig }).then((res) => {
|
||||
if (res.code === 0)
|
||||
ms.success('配置已同步到云端')
|
||||
else
|
||||
ms.error(`配置同步到云端失败${res.msg}`)
|
||||
isSaveing.value = false
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
function handleUploadBackgroundFinish({
|
||||
file,
|
||||
event,
|
||||
}: {
|
||||
file: UploadFileInfo
|
||||
event?: ProgressEvent
|
||||
}) {
|
||||
const res = JSON.parse((event?.target as XMLHttpRequest).response)
|
||||
panelState.panelConfig.backgroundImageSrc = res.data.imageUrl
|
||||
return file
|
||||
}
|
||||
|
||||
function uploadCloud() {
|
||||
setUserConfig({ panel: panelState.panelConfig }).then((res) => {
|
||||
if (res.code === 0)
|
||||
ms.success('配置已同步到云端')
|
||||
else
|
||||
ms.error(`配置同步到云端失败${res.msg}`)
|
||||
})
|
||||
}
|
||||
|
||||
function resetPanelConfig() {
|
||||
panelState.resetPanelConfig()
|
||||
uploadCloud()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-slate-200 rounded-[10px] p-[8px] h-[500px] overflow-auto">
|
||||
<NCard style="border-radius:10px" size="small">
|
||||
<div class="text-slate-500 mb-[5px]">
|
||||
LOGO
|
||||
</div>
|
||||
<NInput v-model:value="panelState.panelConfig.logoText" type="text" show-count :maxlength="20" placeholder="请输入文字" />
|
||||
</NCard>
|
||||
|
||||
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
|
||||
<div class="text-slate-500 mb-[5px]">
|
||||
时钟
|
||||
</div>
|
||||
<div class="flex items-center mt-[5px]">
|
||||
<span class="mr-[10px]">显示秒</span>
|
||||
<NSwitch v-model:value="panelState.panelConfig.clockShowSecond" />
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
|
||||
<div class="text-slate-500 mb-[5px]">
|
||||
壁纸
|
||||
</div>
|
||||
<NUpload
|
||||
action="/api/file/uploadImg"
|
||||
:show-file-list="false"
|
||||
name="imgfile"
|
||||
:headers="{
|
||||
token: authStore.token as string,
|
||||
}"
|
||||
:directory-dnd="true"
|
||||
@finish="handleUploadBackgroundFinish"
|
||||
>
|
||||
<NUploadDragger>
|
||||
<div
|
||||
class="h-[150px] w-[280px] border bg-slate-100 flex justify-center items-center cursor-pointer rounded-[10px]"
|
||||
:style="{ background: `url(${panelState.panelConfig.backgroundImageSrc}) no-repeat`, backgroundSize: 'cover' }"
|
||||
>
|
||||
<div class="text-shadow text-white">
|
||||
点击上传替换图片或拖拽到框内
|
||||
</div>
|
||||
</div>
|
||||
</NUploadDragger>
|
||||
</NUpload>
|
||||
|
||||
<div class="flex items-center mt-[5px]">
|
||||
<span class="mr-[10px]">模糊处理</span>
|
||||
<NSlider v-model:value="panelState.panelConfig.backgroundBlur" class="max-w-[200px]" :step="2" :max="20" />
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
|
||||
<div class="text-slate-500 mb-[5px]">
|
||||
图标
|
||||
</div>
|
||||
<div>
|
||||
样式
|
||||
</div>
|
||||
<div class="flex items-center mt-[5px]">
|
||||
<NSelect v-model:value="panelState.panelConfig.iconStyle" :options="iconTypeOptions" />
|
||||
</div>
|
||||
<div>
|
||||
文字颜色
|
||||
</div>
|
||||
<div class="flex items-center mt-[5px]">
|
||||
<NColorPicker
|
||||
v-model:value="panelState.panelConfig.iconTextColor"
|
||||
:show-alpha="false"
|
||||
size="small"
|
||||
:modes="['hex']"
|
||||
:swatches="[
|
||||
'#000000',
|
||||
'#ffffff',
|
||||
'#18A058',
|
||||
'#2080F0',
|
||||
'#F0A020',
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</NCard>
|
||||
|
||||
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
|
||||
<NPopconfirm
|
||||
@positive-click="resetPanelConfig"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="small" quaternary type="error">
|
||||
重置
|
||||
</NButton>
|
||||
</template>
|
||||
确定要重置这些样式吗?
|
||||
</NPopconfirm>
|
||||
|
||||
<NButton size="small" quaternary type="success" class="ml-[10px]" @click="uploadCloud">
|
||||
立即同步到云端
|
||||
</NButton>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-shadow{
|
||||
text-shadow: 0px 0px 5px gray;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, useDialog, useMessage } from 'naive-ui'
|
||||
import { useAuthStore, usePanelState, useUserStore } from '@/store'
|
||||
import { logout } from '@/api'
|
||||
import { router } from '@/router'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
const panelState = usePanelState()
|
||||
|
||||
const ms = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
async function logoutApi() {
|
||||
await logout()
|
||||
userStore.resetUserInfo()
|
||||
authStore.removeToken()
|
||||
panelState.removeState()
|
||||
ms.success('您已经安全退出,期待与你再次相见!')
|
||||
router.push({ path: '/login' })
|
||||
}
|
||||
|
||||
function handleLogiut() {
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '你确定要退出登录',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
logoutApi()
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-slate-200 rounded-[10px] p-[8px]">
|
||||
<NCard style="border-radius:10px" size="small">
|
||||
<div class="text-slate-500 mb-[5px]">
|
||||
账号/邮箱
|
||||
</div>
|
||||
{{ userStore.userInfo.username }}
|
||||
</NCard>
|
||||
|
||||
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
|
||||
<div class="text-slate-500 mb-[5px]">
|
||||
昵称
|
||||
</div>
|
||||
{{ userStore.userInfo.name }}
|
||||
</NCard>
|
||||
|
||||
<NCard style="border-radius:10px" class="mt-[10px]" size="small">
|
||||
<NButton size="small" quaternary type="error" @click="handleLogiut">
|
||||
退出登录
|
||||
</NButton>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,192 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, onMounted, reactive, ref } from 'vue'
|
||||
import { NButton, NDataTable, NDropdown, useDialog, useMessage } from 'naive-ui'
|
||||
import type { DataTableColumns, PaginationProps } from 'naive-ui'
|
||||
import EditUser from './EditUser/index.vue'
|
||||
import { deletes as usersDeletes, getList as usersGetList } from '@/api/panel/users'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
const message = useMessage()
|
||||
const userStore = useUserStore()
|
||||
const tableIsLoading = ref<boolean>(false)
|
||||
const editUserDialogShow = ref<boolean>(false)
|
||||
const keyWord = ref<string>()
|
||||
const editUserUserInfo = ref<User.Info>()
|
||||
const dialog = useDialog()
|
||||
|
||||
const createColumns = ({
|
||||
update,
|
||||
}: {
|
||||
update: (row: User.Info) => void
|
||||
}): DataTableColumns<User.Info> => {
|
||||
return [
|
||||
{
|
||||
title: '账号',
|
||||
key: 'username',
|
||||
render(row: User.Info) {
|
||||
if (row.username === userStore.userInfo.username)
|
||||
return `${row.username} (当前账号)`
|
||||
return row.username
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '昵称',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: '',
|
||||
render(row) {
|
||||
const btn = h(
|
||||
NButton,
|
||||
{
|
||||
strong: true,
|
||||
tertiary: true,
|
||||
size: 'small',
|
||||
},
|
||||
{
|
||||
default() {
|
||||
return h(
|
||||
SvgIcon, {
|
||||
icon: 'mingcute:more-1-fill',
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return h(NDropdown, {
|
||||
trigger: 'click',
|
||||
onSelect(key: string | number) {
|
||||
console.log(key)
|
||||
switch (key) {
|
||||
case 'update':
|
||||
update(row)
|
||||
break
|
||||
case 'delete':
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: `你确定删除${row.name}(${row.username})?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
deletes([row.id as number])
|
||||
},
|
||||
})
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
options: [
|
||||
{
|
||||
label: '修改信息',
|
||||
key: 'update',
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
key: 'delete',
|
||||
},
|
||||
],
|
||||
}, { default: () => btn })
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const userList = ref<User.Info[]>()
|
||||
|
||||
const columns = createColumns({
|
||||
update(row: User.Info) {
|
||||
editUserUserInfo.value = row
|
||||
editUserDialogShow.value = true
|
||||
},
|
||||
})
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 30, 50, 100, 200],
|
||||
pageSize: 10,
|
||||
itemCount: 0,
|
||||
onChange: (page: number) => {
|
||||
pagination.page = page
|
||||
getList(null)
|
||||
},
|
||||
onUpdatePageSize: (pageSize: number) => {
|
||||
pagination.pageSize = pageSize
|
||||
pagination.page = 1
|
||||
getList(null)
|
||||
},
|
||||
prefix(item: PaginationProps) {
|
||||
return `共 ${item.itemCount} 位用户`
|
||||
},
|
||||
})
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
getList(page)
|
||||
}
|
||||
|
||||
// 添加
|
||||
function handleAdd() {
|
||||
editUserDialogShow.value = true
|
||||
editUserUserInfo.value = {}
|
||||
}
|
||||
|
||||
function handelDone() {
|
||||
editUserDialogShow.value = false
|
||||
message.success('操作成功')
|
||||
getList(null)
|
||||
}
|
||||
|
||||
async function getList(page: number | null) {
|
||||
tableIsLoading.value = true
|
||||
const req: AdminUserManage.GetListRequest = {
|
||||
page: page || pagination.page,
|
||||
limit: pagination.pageSize,
|
||||
}
|
||||
if (keyWord.value !== '')
|
||||
req.keyWord = keyWord.value
|
||||
|
||||
const { data } = await usersGetList<Common.ListResponse<User.Info[]>>(req)
|
||||
pagination.itemCount = data.count
|
||||
if (data.list)
|
||||
userList.value = data.list
|
||||
tableIsLoading.value = false
|
||||
}
|
||||
|
||||
async function deletes(ids: number[]) {
|
||||
const { code } = await usersDeletes(ids)
|
||||
if (code === 0) {
|
||||
message.success('已删除')
|
||||
getList(null)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList(null)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[500px] overflow-auto">
|
||||
<div class="mb-[10px]">
|
||||
<NButton type="primary" size="small" ghost @click="handleAdd">
|
||||
添加
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="userList"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
:loading="tableIsLoading"
|
||||
:remote="true"
|
||||
|
||||
@update:page="handlePageChange"
|
||||
/>
|
||||
<EditUser v-model:visible="editUserDialogShow" :user-info="editUserUserInfo" @done="handelDone" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
import Result from './Result/index.vue'
|
||||
import EditItem from './EditItem/index.vue'
|
||||
import Setting from './Setting/index.vue'
|
||||
|
||||
export {
|
||||
Result, EditItem, Setting,
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NButtonGroup, NDropdown, NEllipsis, NGrid, NGridItem, NModal, NSkeleton, NSpin, useMessage } from 'naive-ui'
|
||||
import { nextTick, onMounted, ref } from 'vue'
|
||||
import { EditItem, Setting } from './components'
|
||||
import { Clock } from '@/components/deskModule'
|
||||
import { ItemIcon, SvgIcon } from '@/components/common'
|
||||
import { getListByGroupId } from '@/api/panel/itemIcon'
|
||||
import { getInfo } from '@/api/system/user'
|
||||
import { usePanelState, useUserStore } from '@/store'
|
||||
import { PanelStateNetworkModeEnum } from '@/enum'
|
||||
import { setTitle } from '@/utils/cmn'
|
||||
|
||||
const ms = useMessage()
|
||||
const panelState = usePanelState()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const editItemInfoShow = ref<boolean>(false)
|
||||
const editItemInfoData = ref<Panel.ItemInfo | null>(null)
|
||||
const windowShow = ref<boolean>(false)
|
||||
const windowSrc = ref<string>('')
|
||||
const windowTitle = ref<string>('')
|
||||
|
||||
const windowIframeRef = ref(null)
|
||||
const windowIframeIsLoad = ref<boolean>(false)
|
||||
|
||||
const dropdownMenuX = ref(0)
|
||||
const dropdownMenuY = ref(0)
|
||||
const dropdownShow = ref(false)
|
||||
const currentRightSelectItem = ref<Panel.ItemInfo | null>(null)
|
||||
|
||||
const settingModalShow = ref(false)
|
||||
|
||||
const dropdownMenuOptions = [
|
||||
{
|
||||
label: '新窗口打开',
|
||||
key: 'newWindows',
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
key: 'edit',
|
||||
},
|
||||
]
|
||||
const items = ref<Panel.ItemInfo[]>()
|
||||
|
||||
function handleAddAppClick() {
|
||||
editItemInfoData.value = null
|
||||
editItemInfoShow.value = true
|
||||
}
|
||||
|
||||
function handleItemClick(item: Panel.ItemInfo) {
|
||||
let jumpUrl = ''
|
||||
|
||||
if (item)
|
||||
jumpUrl = (panelState.networkMode === PanelStateNetworkModeEnum.lan ? item.lanUrl : item.url) as string
|
||||
if (item.lanUrl === '')
|
||||
jumpUrl = item.url
|
||||
|
||||
switch (item.openMethod) {
|
||||
case 1:
|
||||
window.location.href = jumpUrl
|
||||
break
|
||||
case 2:
|
||||
window.open(jumpUrl)
|
||||
break
|
||||
case 3:
|
||||
windowShow.value = true
|
||||
windowSrc.value = jumpUrl
|
||||
windowTitle.value = item.title
|
||||
windowIframeIsLoad.value = true
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handWindowIframeIdLoad(payload: Event) {
|
||||
windowIframeIsLoad.value = false
|
||||
}
|
||||
|
||||
function getList() {
|
||||
getListByGroupId<Common.ListResponse<Panel.ItemInfo[]>>().then((res) => {
|
||||
if (res.code === 0)
|
||||
items.value = res.data.list
|
||||
})
|
||||
}
|
||||
|
||||
function handleSelect(key: string | number) {
|
||||
dropdownShow.value = false
|
||||
// console.log(currentRightSelectItem, key)
|
||||
let jumpUrl = panelState.networkMode === PanelStateNetworkModeEnum.lan ? currentRightSelectItem.value?.lanUrl : currentRightSelectItem.value?.url
|
||||
if (currentRightSelectItem.value?.lanUrl === '')
|
||||
jumpUrl = currentRightSelectItem.value.url
|
||||
switch (key) {
|
||||
case 'newWindows':
|
||||
window.open(jumpUrl)
|
||||
break
|
||||
case 'edit':
|
||||
// 这里有个奇怪的问题,如果不使用{...}的方式 父组件的值会同步修改 标记一下
|
||||
editItemInfoData.value = { ...currentRightSelectItem.value } as Panel.ItemInfo
|
||||
editItemInfoShow.value = true
|
||||
break
|
||||
case 'del':
|
||||
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextMenu(e: MouseEvent, item: Panel.ItemInfo) {
|
||||
e.preventDefault()
|
||||
currentRightSelectItem.value = item
|
||||
dropdownShow.value = false
|
||||
nextTick().then(() => {
|
||||
dropdownShow.value = true
|
||||
dropdownMenuX.value = e.clientX
|
||||
dropdownMenuY.value = e.clientY
|
||||
})
|
||||
}
|
||||
|
||||
function onClickoutside() {
|
||||
// message.info('clickoutside')
|
||||
dropdownShow.value = false
|
||||
}
|
||||
|
||||
function handleEditSuccess(item: Panel.ItemInfo) {
|
||||
getList()
|
||||
}
|
||||
|
||||
function handleChangeNetwork(mode: PanelStateNetworkModeEnum) {
|
||||
panelState.setNetworkMode(mode)
|
||||
if (mode === PanelStateNetworkModeEnum.lan)
|
||||
ms.success('已经切换成局域网模式,此时再点击已填写局域网地址的图标将跳转至局域网地址(此配置仅保存在本地)')
|
||||
|
||||
else
|
||||
ms.success('已经切换成互联网模式(此配置仅保存在本地)')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
|
||||
// 获取用户信息
|
||||
getInfo<User.Info>().then((res) => {
|
||||
if (res.code === 0)
|
||||
userStore.updateUserInfo(res.data)
|
||||
})
|
||||
|
||||
// 更新同步云端配置
|
||||
panelState.updatePanelConfigByCloud()
|
||||
|
||||
// 设置标题
|
||||
if (panelState.panelConfig.logoText)
|
||||
setTitle(panelState.panelConfig.logoText)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full sun-main ">
|
||||
<div
|
||||
class="cover"
|
||||
:style="{
|
||||
filter: `blur(${panelState.panelConfig.backgroundBlur}px)`,
|
||||
background: `url(${panelState.panelConfig.backgroundImageSrc}) no-repeat`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}"
|
||||
/>
|
||||
<div class="absolute w-full h-full overflow-auto">
|
||||
<div class="p-2.5 max-w-[1200px] mx-auto mt-[10%]">
|
||||
<!-- 头 -->
|
||||
<div class="mx-[auto] w-[80%]">
|
||||
<div class="flex mx-[auto] items-center justify-center text-white">
|
||||
<div>
|
||||
<span class="text-5xl font-bold text-shadow">
|
||||
{{ panelState.panelConfig.logoText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-2xl mx-[10px]">
|
||||
|
|
||||
</div>
|
||||
<div class="text-shadow">
|
||||
<Clock :hide-second="!panelState.panelConfig.clockShowSecond" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="flex mt-[20px] mx-auto w-[80%]">
|
||||
<SearchBox />
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- 图标 -->
|
||||
<div class="mt-[50px]">
|
||||
<!-- 详情图标 -->
|
||||
<div v-if="panelState.panelConfig.iconStyle === 0">
|
||||
<NGrid :x-gap="15" :y-gap="15" item-responsive cols="1 200:1 400:2 600:3 800:4 1000:5 1200:6">
|
||||
<NGridItem v-for="(item, index) in items" :key="index">
|
||||
<div @contextmenu="(e) => handleContextMenu(e, item)">
|
||||
<div
|
||||
class="w-full rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] bg-[#2a2a2a6b] flex"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<div class="w-[70px]">
|
||||
<ItemIcon :item-icon="item.icon" />
|
||||
</div>
|
||||
<div class="text-white m-[8px_8px_0_8px]" :style="{ color: panelState.panelConfig.iconTextColor }">
|
||||
<div>
|
||||
<NEllipsis>
|
||||
{{ item.title }}
|
||||
</NEllipsis>
|
||||
</div>
|
||||
<div>
|
||||
<NEllipsis :line-clamp="2" class="text-xs">
|
||||
{{ item.description }}
|
||||
</NEllipsis>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
|
||||
<NGridItem>
|
||||
<div>
|
||||
<div
|
||||
class="w-full rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] bg-[#2a2a2a6b] flex"
|
||||
@click="handleAddAppClick"
|
||||
>
|
||||
<ItemIcon :item-icon="{ itemType: 3, text: 'subway:add', bgColor: '#00000000' }" />
|
||||
<div class="text-white m-[8px]" :style="{ color: panelState.panelConfig.iconTextColor }">
|
||||
<div>
|
||||
<NEllipsis>
|
||||
添加图标
|
||||
</NEllipsis>
|
||||
</div>
|
||||
|
||||
<div class="text text-xs">
|
||||
<NEllipsis>
|
||||
新增一个新的图标
|
||||
</NEllipsis>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
|
||||
<!-- APP图标宫型盒子 -->
|
||||
<div v-if="panelState.panelConfig.iconStyle === 1">
|
||||
<NGrid :x-gap="12" :y-gap="8" item-responsive cols="3 300:4 600:6 900:8">
|
||||
<NGridItem v-for="(item, index) in items" :key="index">
|
||||
<div @contextmenu="(e) => handleContextMenu(e, item)">
|
||||
<div
|
||||
class="w-[70px] h-[70px] mx-auto rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)] bg-[#2a2a2a6b]"
|
||||
@click="handleItemClick(item)"
|
||||
>
|
||||
<ItemIcon :item-icon="item.icon" />
|
||||
</div>
|
||||
<div class="text-center app-icon-text-shadow cursor-pointer mt-[2px]" :style="{ color: panelState.panelConfig.iconTextColor }" @click="handleItemClick(item)">
|
||||
<span>{{ item.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
|
||||
<NGridItem>
|
||||
<div>
|
||||
<div class="w-[70px] h-[70px] mx-auto rounded-2xl cursor-pointer transition-all duration-200 hover:shadow-[0_0_20px_10px_rgba(0,0,0,0.2)]" @click="handleAddAppClick">
|
||||
<ItemIcon :item-icon="{ itemType: 3, text: 'subway:add', bgColor: '#343434' }" />
|
||||
</div>
|
||||
<div class="text-center app-icon-text-shadow cursor-pointer mt-[2px]" :style="{ color: panelState.panelConfig.iconTextColor }" @click="handleAddAppClick">
|
||||
添加图标
|
||||
</div>
|
||||
</div>
|
||||
</NGridItem>
|
||||
</NGrid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<NDropdown
|
||||
placement="bottom-start"
|
||||
trigger="manual"
|
||||
:x="dropdownMenuX"
|
||||
:y="dropdownMenuY"
|
||||
:options="dropdownMenuOptions"
|
||||
:show="dropdownShow"
|
||||
:on-clickoutside="onClickoutside"
|
||||
@select="handleSelect"
|
||||
/>
|
||||
|
||||
<!-- 悬浮按钮 -->
|
||||
<div class="fixed-element shadow-[0_0_10px_2px_rgba(0,0,0,0.2)]">
|
||||
<NButtonGroup vertical>
|
||||
<NButton v-if="panelState.networkMode === PanelStateNetworkModeEnum.lan" color="#2a2a2a6b" title="当前:局域网模式,点击切换成互联网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.wan)">
|
||||
<template #icon>
|
||||
<SvgIcon class="text-white font-xl" icon="material-symbols:lan-outline" />
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton v-if="panelState.networkMode === PanelStateNetworkModeEnum.wan" color="#2a2a2a6b" title="当前:互联网模式,点击切换成局域网模式" @click="handleChangeNetwork(PanelStateNetworkModeEnum.lan)">
|
||||
<template #icon>
|
||||
<SvgIcon class="text-white font-xl" icon="mdi:wan" />
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton color="#2a2a2a6b" @click="settingModalShow = !settingModalShow">
|
||||
<template #icon>
|
||||
<SvgIcon class="text-white font-xl" icon="ep:setting" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NButtonGroup>
|
||||
|
||||
<Setting v-model:visible="settingModalShow" />
|
||||
</div>
|
||||
|
||||
<EditItem v-model:visible="editItemInfoShow" :item-info="editItemInfoData" @done="handleEditSuccess" />
|
||||
|
||||
<!-- 新窗口 -->
|
||||
<NModal
|
||||
v-model:show="windowShow"
|
||||
:mask-closable="false"
|
||||
preset="card"
|
||||
style="max-width: 1000px;height: 600px;border-radius: 1rem;"
|
||||
:bordered="false"
|
||||
size="small"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[20px]">
|
||||
{{ windowTitle }}
|
||||
</span>
|
||||
|
||||
<NSpin v-if="windowIframeIsLoad" size="small" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full h-full rounded-2xl overflow-hidden border">
|
||||
<NSkeleton v-if="windowIframeIsLoad" height="100%" width="100%" />
|
||||
<iframe v-show="!windowIframeIsLoad" id="windowIframeId" ref="windowIframeRef" :src="windowSrc" class="w-full h-full" frameborder="0" @load="handWindowIframeIdLoad" />
|
||||
</div>
|
||||
</NModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
body,html{
|
||||
overflow: hidden;
|
||||
background-color: rgb(54, 54, 54);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.sun-main{
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cover{
|
||||
position:absolute;
|
||||
width:100%;
|
||||
height:100%;
|
||||
overflow: hidden;
|
||||
/* background: url(@/assets/start_sky.jpg) no-repeat; */
|
||||
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.text-shadow{
|
||||
text-shadow: 2px 2px 50px rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.app-icon-text-shadow{
|
||||
text-shadow: 2px 2px 5px rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.fixed-element {
|
||||
position: fixed; /* 将元素固定在屏幕上 */
|
||||
right: 30px; /* 距离屏幕顶部的距离 */
|
||||
bottom: 50px; /* 距离屏幕左侧的距离 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NCard, NForm, NFormItem, NGradientText, NInput, useMessage } from 'naive-ui'
|
||||
import { ref } from 'vue'
|
||||
import { login } from '@/api'
|
||||
import { useAuthStore, useUserStore } from '@/store'
|
||||
import { router } from '@/router'
|
||||
import { Captcha, SvgIcon } from '@/components/common'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const authStore = useAuthStore()
|
||||
const ms = useMessage()
|
||||
const isShowCaptcha = ref<boolean>(false)
|
||||
const isShowRegister = ref<boolean>(false)
|
||||
|
||||
const captchaRef = ref()
|
||||
|
||||
const form = ref<Login.LoginReqest>({
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const loginPost = async () => {
|
||||
const res = await login<Login.LoginResponse>(form.value)
|
||||
|
||||
userStore.updateUserInfo(res.data)
|
||||
|
||||
if (res.code === 0) {
|
||||
authStore.setToken(res.data.token)
|
||||
ms.success(`Hi ${res.data.name},欢迎回来!`)
|
||||
router.push({ path: '/' })
|
||||
}
|
||||
else {
|
||||
captchaRef.value.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
// 点击登录按钮触发
|
||||
loginPost()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<NCard class="login-card">
|
||||
<div class="login-title">
|
||||
<NGradientText :size="30" type="success">
|
||||
{{ $t('common.appName') }}
|
||||
</NGradientText>
|
||||
</div>
|
||||
<NForm :model="form" label-width="100px">
|
||||
<NFormItem>
|
||||
<NInput v-model:value="form.username" placeholder="请输入邮箱地址作为账号">
|
||||
<template #prefix>
|
||||
<SvgIcon icon="ph:user-bold" />
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem>
|
||||
<NInput v-model:value="form.password" type="password" placeholder="请输入密码">
|
||||
<template #prefix>
|
||||
<SvgIcon icon="mdi:password-outline" />
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem v-if="isShowCaptcha">
|
||||
<div class="w-[120px] h-[34px] mr-[20px] rounded border flex cursor-pointer">
|
||||
<Captcha ref="captchaRef" src="/api/captcha/getImage" />
|
||||
</div>
|
||||
<NInput v-model:value="form.vcode" type="text" placeholder="请输入图像验证码" />
|
||||
</NFormItem>
|
||||
<NFormItem style="margin-top: 10px">
|
||||
<NButton type="primary" block @click="handleSubmit">
|
||||
登录
|
||||
</NButton>
|
||||
</NFormItem>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<NButton v-if="isShowRegister" quaternary type="info" class="flex" @click="$router.push({ path: '/register' })">
|
||||
注册
|
||||
</NButton>
|
||||
<!-- <NButton quaternary type="info" class="flex" @click="$router.push({ path: '/resetPassword' })">
|
||||
忘记密码?
|
||||
</NButton> -->
|
||||
</div>
|
||||
</NForm>
|
||||
</NCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background-color: #f2f6ff;
|
||||
}
|
||||
|
||||
/* 夜间模式 */
|
||||
.dark .login-container{
|
||||
background-color: rgb(43, 43, 43);
|
||||
}
|
||||
|
||||
@media (min-width: 600px) {
|
||||
.login-card {
|
||||
width: auto;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
.login-button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-card {
|
||||
margin: 20px;
|
||||
min-width:400px;
|
||||
}
|
||||
|
||||
.login-title{
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user