This commit is contained in:
@zuopngfei 2025-05-19 16:44:51 +08:00
parent fe8f8be86a
commit 62d2ead615
56 changed files with 3935 additions and 0 deletions

View File

@ -0,0 +1,28 @@
client_max_body_size 100m;
server {
listen 8080;
server_name localhost;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root /usr/share/nginx/html;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
try_files $uri $uri/ /index.html;
}
location /api {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
rewrite ^/api/(.*)$ /$1 break; #重写
proxy_pass http://129.204.101.51:9999; # 设置代理服务器的协议和地址
}
location /api/swagger/index.html {
proxy_pass http://129.204.101.51:9999/swagger/index.html;
}
}

5
.env.development Normal file
View File

@ -0,0 +1,5 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
VITE_APP_BASE_API = 'https://tools.1024tool.vip/'
VITE_SERVE = "https://tools.1024tool.vip/"
# VITE_SERVE = "http://192.168.210.29:18100/"

5
.env.production Normal file
View File

@ -0,0 +1,5 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'production'
VITE_APP_BASE_API = 'https://tools.1024tool.vip/'
VITE_SERVE = "https://tools.1024tool.vip/"

5
.env.test Normal file
View File

@ -0,0 +1,5 @@
NODE_ENV = 'development'
VITE_APP_BASE_API = 'http://129.204.101.51:9999/'
VITE_SERVE = "http://129.204.101.51:9999/"

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
components.d.ts
node_modules
dist
dist-ssr
*.local
.vite
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.zip

24
Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM registry.cn-shanghai.aliyuncs.com/server1024/node:16.17.1 AS build-stage
WORKDIR /mht/
COPY . .
# 设置国内源并调试网络问题
RUN npm config set registry https://registry.npmmirror.com
RUN npm config set network-timeout 600000
RUN npm install --legacy-peer-deps
# 确保 vite 已经全局安装
RUN npm install -g vite
# 检查 vite 是否安装成功
RUN vite --version
# 运行构建命令
RUN npm run build:prod
FROM registry.cn-shanghai.aliyuncs.com/server1024/nginx:base AS production-stage
COPY .docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf
COPY --from=build-stage /mht/dist /usr/share/nginx/html
RUN ls -al /usr/share/nginx/html

11
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,11 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
const ElNotification: typeof import('element-plus/es')['ElNotification']
}

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!doctype html>
<html lang="zh_CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>儿科后台管理</title>
<meta name="keywords" content="儿科后台管理">
<meta name="description" content="儿科后台管理">
<!-- <script src="//at.alicdn.com/t/c/font_4896732_5thx9wsstok.js"></script> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

69
package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "mihoutao",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --open",
"build": "vite build",
"build:test": "vite build --mode test",
"build:prod": "vite build --mode production",
"build:no": "vite build",
"preview": "vite preview",
"lint": "eslint src",
"fix": "eslint src --fix",
"format": "prettier --write \"./**/*.{html,vue,ts,js,json,md}\"",
"lint:eslint": "eslint src/**/*.{ts,vue} --cache --fix",
"lint:style": "stylelint src/**/*.{css,scss,vue} --cache --fix"
},
"dependencies": {
"@element-plus/icons-vue": "2.1.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "next",
"axios": "^1.4.0",
"default-passive-events": "^2.0.0",
"element-plus": "^2.8.0",
"mitt": "^3.0.1",
"moment": "^2.29.4",
"nprogress": "^0.2.0",
"pinia": "^2.1.6",
"pinia-plugin-persistedstate": "^3.2.0",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@babel/eslint-parser": "^7.22.9",
"@iconify-json/ep": "^1.1.11",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"@vitejs/plugin-vue": "^4.2.3",
"eslint": "^8.45.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-vue": "^9.15.1",
"mockjs": "^1.1.0",
"postcss": "^8.4.27",
"postcss-html": "^1.5.0",
"postcss-pxtorem": "^6.1.0",
"postcss-scss": "^4.0.6",
"sass": "^1.64.1",
"sass-loader": "^13.3.2",
"stylelint": "^15.10.2",
"stylelint-config-prettier": "^9.0.5",
"stylelint-config-recess-order": "^4.3.0",
"stylelint-config-recommended-scss": "^12.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-config-standard-scss": "^10.0.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-order": "^6.0.3",
"stylelint-scss": "^5.0.1",
"typescript": "^5.0.2",
"unplugin-auto-import": "^0.16.6",
"unplugin-icons": "^0.16.5",
"unplugin-vue-components": "^0.25.1",
"vite": "^4.4.5",
"vite-plugin-mock": "2.9.6",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.8.5"
}
}

8
postcss.config.cjs Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
// plugins: {
// 'postcss-pxtorem': {
// rootValue: 100, // 设置根元素字体大小1rem = 16px
// propList: ['*'], // 将所有属性都转换成rem
// },
// },
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

20
src/App.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<router-view v-slot="{ Component }">
<component :is="Component" />
</router-view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
onMounted(() => {
})
</script>
<style scoped>
</style>
<style lang="scss">
#app {
}
</style>

91
src/api/login.ts Normal file
View File

@ -0,0 +1,91 @@
import request from '@/utils/request'
// 登录
export const userLogin = (data: any) => {
return request({
url: `api/customer/login`,
method: 'post',
data
})
}
// 注册
export const userRegister = (data: any) => {
return request({
url: `api/customer/register`,
method: 'post',
data
})
}
// 发送邮箱验证码
export const sendEmail = (data: any) => {
return request({
url: `api/customer/send_activation_email`,
method: 'post',
data
})
}
// 获取用户信息
export const getUserInfo = () => {
return request({
url: `api/customer/info`,
method: 'get'
})
}
// 激活账号
export const verifyCode = (code: any) => {
return request({
url: `api/customer/verify_activation_email/${code}`,
method: 'get'
})
}
// 扫码登录
export const getRr = (data) => {
return request({
url: `api/customer/qr`,
method: 'post',
data
})
}
// 轮询登录结果
export const getRrLogin = (data) => {
return request({
url: `api/customer/qr_login`,
method: 'post',
data
})
}
export const getUserSearch = (data) => {
return request({
url: `api/customer/search`,
method: 'post',
data
})
}
export const setUserRecharge = (data) => {
return request({
url: `api/customer/recharge`,
method: 'post',
data
})
}
// 充值记录
export const rechargeList = (data) => {
return request({
url: `api/customer/recharge/list`,
method: 'get',
params: data
})
}

91
src/api/news.ts Normal file
View File

@ -0,0 +1,91 @@
import request from '@/utils/request'
// 登录
export const userLogin = (data: any) => {
return request({
url: `api/customer/login`,
method: 'post',
data
})
}
// 注册
export const userRegister = (data: any) => {
return request({
url: `api/customer/register`,
method: 'post',
data
})
}
// 发送邮箱验证码
export const sendEmail = (data: any) => {
return request({
url: `api/customer/send_activation_email`,
method: 'post',
data
})
}
// 获取用户信息
export const getUserInfo = () => {
return request({
url: `api/customer/info`,
method: 'get'
})
}
// 激活账号
export const verifyCode = (code: any) => {
return request({
url: `api/customer/verify_activation_email/${code}`,
method: 'get'
})
}
// 扫码登录
export const getRr = (data) => {
return request({
url: `api/customer/qr`,
method: 'post',
data
})
}
// 轮询登录结果
export const getRrLogin = (data) => {
return request({
url: `api/customer/qr_login`,
method: 'post',
data
})
}
export const getUserSearch = (data) => {
return request({
url: `api/customer/search`,
method: 'post',
data
})
}
export const setUserRecharge = (data) => {
return request({
url: `api/customer/recharge`,
method: 'post',
data
})
}
// 充值记录
export const rechargeList = (data) => {
return request({
url: `api/customer/recharge/list`,
method: 'get',
params: data
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

View File

@ -0,0 +1,35 @@
<template>
<div>
<svg :style="{ width: width, height: height }">
<use :xlink:href="prefix + name" :fill="color"></use>
</svg>
</div>
</template>
<script setup lang="ts">
defineProps({
//xlink:href
prefix: {
type: String,
default: '#icon-',
},
//svg
name: String,
//svg
color: {
type: String,
default: '',
},
//svg
width: {
type: String,
default: '16px',
},
//svg
height: {
type: String,
default: '16px',
},
})
</script>
<style scoped></style>

20
src/components/index.ts Normal file
View File

@ -0,0 +1,20 @@
import SvgIcon from './SvgIcon/index.vue'
import type { App, Component } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const allGlobalComponent: Component = { SvgIcon }
export default {
install(app: App) {
Object.keys(allGlobalComponent).forEach((key: string) => {
// 注册为全局组件
app.component(key, allGlobalComponent[key])
})
// 将 element-plus 的图标注册为全局组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
},
}

15
src/directive/has.ts Normal file
View File

@ -0,0 +1,15 @@
// 按钮权限的实现
import pinia from '@/store'
import useUserStore from '@/store/modules/user'
export const isHasButton = (app: any) => {
// 自定义指令
app.directive('has', {
mounted(el: any, options: any) {
const userStore = useUserStore(pinia)
if (!userStore.buttons.includes(options.value)) {
el.parentNode.removeChild(el)
}
}
})
}

2
src/eventBus/index.ts Normal file
View File

@ -0,0 +1,2 @@
import mitt from 'mitt'
export default mitt()

129
src/layout/header/index.vue Normal file
View File

@ -0,0 +1,129 @@
<template>
<el-header>
<div class="logo" @click="goHome">
<img src="@/assets/images/logo.svg" />
猕猴桃
</div>
<div class="tab">
<ul>
<li :class="{ active: actIndex == 0 }" @click="handelClick(0)">公众号</li>
<!-- <li :class="{ active: actIndex == 1 }" @click="handelClick(1)">视频号</li>
<li @click="toWebsite">AI私域</li>
<li :class="{ active: actIndex == 1 }" @click="handelClick(2)">API</li> -->
<li :class="{ active: actIndex == 3 }" @click="handelClick(3)">AI爆文生成</li>
</ul>
</div>
<Setting></Setting>
</el-header>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { GET_TOKEN } from '@/utils/token';
import Setting from '@/layout/setting/index.vue'
import emitter from '@/eventBus'
const router = useRouter()
const route = useRoute()
const path = computed(() => {
return route.path
})
watch(path, (val) => {
if(val == '/dash'){
actIndex.value = 0
} else if(val == '/aiArticle'){
actIndex.value = 3
} else {
actIndex.value = null
}
})
const goHome = () => {
router.push('/')
}
const toWebsite = () => {
open('https://7vbot.1024tool.vip/')
}
const actIndex = ref(null)
const handelClick = (index) => {
console.log(index)
if(index == 0){
if (GET_TOKEN()) {
actIndex.value = 0
router.push('/dash')
} else {
emitter.emit('userLogin', 1)
}
} else if(index == 1) {
ElNotification({
type: 'warning',
message: ' 视频号开发中,敬请期待!',
})
} else if(index == 2){
open('https://apifox.com/apidoc/shared-a0bce534-8fcf-4b6e-a297-5b670c19a2b4/241940441e0')
} else if(index == 3){
if (GET_TOKEN()) {
actIndex.value = 3
router.push('/aiArticle')
} else {
emitter.emit('userLogin', 3)
}
}
}
</script>
<style lang="scss" scoped>
.el-header {
position: fixed;
width: 100%;
height: 70px;
left: 0;
top: 0;
line-height: 70px;
padding: 0 100px;
overflow: hidden;
box-shadow: var(--el-box-shadow-light);
// background-color: #fff;
z-index: 9;
background-image: radial-gradient(transparent 1px,var(--bg-color) 1px);
background-size: 4px 4px;
backdrop-filter: saturate(50%) blur(8px);
.logo {
float: left;
cursor: pointer;
img {
height: 36px;
vertical-align: middle
}
}
}
.tab{
position: absolute;
left: 50%;
// margin-left: -100px;
transform: translateX(-50%);
li{
float: left;
padding: 0 20px;
// width: 100px;
text-align: center;
line-height: 70px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
&:hover{
color: var(--el-color-primary-light-3);
}
}
.active{
color: var(--el-color-primary);
}
}
</style>

120
src/layout/home/header.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<header>
<div class="logo" @click="goHome">
<!-- <img src="@/assets/images/logo.svg" /> -->
<!-- 患者管理系统 -->
</div>
<div class="tab">
</div>
<Setting></Setting>
</header>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { GET_TOKEN } from '@/utils/token';
import Setting from '@/layout/setting/index.vue'
import emitter from '@/eventBus'
const router = useRouter()
const route = useRoute()
const path = computed(() => {
return route.path
})
watch(path, (val) => {
// if(val == '/dash'){
// actIndex.value = 0
// } else if(val == '/aiArticle'){
// actIndex.value = 3
// } else {
// actIndex.value = null
// }
})
const goHome = () => {
router.push('/')
}
const toWebsite = () => {
open('https://7vbot.1024tool.vip/')
}
const actIndex = ref(null)
const handelClick = (index) => {
console.log(index)
if(index == 0){
if (GET_TOKEN()) {
actIndex.value = 0
router.push('/dash')
} else {
emitter.emit('userLogin', 1)
}
} else if(index == 1) {
ElNotification({
type: 'warning',
message: ' 视频号开发中,敬请期待!',
})
} else if(index == 2){
open('https://apifox.com/apidoc/shared-a0bce534-8fcf-4b6e-a297-5b670c19a2b4/241940441e0')
} else if(index == 3){
if (GET_TOKEN()) {
actIndex.value = 3
router.push('/aiArticle')
} else {
emitter.emit('userLogin', 3)
}
}
}
</script>
<style lang="scss" scoped>
header {
height: 60px;
position: relative;
line-height: 60px;
padding: 0px 30px;
overflow: hidden;
border-bottom: 1px solid var(--el-border-color-light);
// background-color: #fff;
z-index: 9;
// background-image: radial-gradient(transparent 1px,var(--bg-color) 1px);
// background-size: 4px 4px;
// backdrop-filter: saturate(50%) blur(8px);
.logo {
float: left;
cursor: pointer;
img {
height: 36px;
vertical-align: middle
}
}
}
.tab{
position: absolute;
left: 50%;
// margin-left: -100px;
transform: translateX(-50%);
li{
float: left;
padding: 0 20px;
// width: 100px;
text-align: center;
line-height: 60px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
&:hover{
color: var(--el-color-primary-light-3);
}
}
.active{
color: var(--el-color-primary);
}
}
</style>

80
src/layout/home/index.vue Normal file
View File

@ -0,0 +1,80 @@
<template>
<div class="common-layout">
<el-container>
<el-aside width="200px">
<AdminMenu />
</el-aside>
<el-container class="admin-container">
<el-header>
<Tabbar />
</el-header>
<el-main>
<router-view v-slot="{ Component }">
<Transition name="fade" mode="out-in" enter-from-class="fade-enter">
<keep-alive :include="cachedComponents">
<component :is="Component" />
</keep-alive>
</Transition>
</router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup>
import { onMounted, computed } from 'vue'
import AdminMenu from './menu.vue'
import Tabbar from './header.vue'
import { useRoute, useRouter } from 'vue-router';
const route = useRoute()
const router = useRouter();
const cachedComponents = computed(() =>
router.getRoutes().filter((r) => r.meta.KeepAlive).map((r) => r.name)
);
</script>
<style lang="scss" scoped>
.common-layout {
height: 100vh;
overflow: hidden;
}
.el-container{
height: 100%;
}
.el-header {
padding: 0;
}
.el-aside {
width: 200px;
height: 100%;
overflow: initial;
border-right: 1px solid var(--el-border-color-light);
}
.el-main {
height: 100%;
background-color: #fff;
min-width: 1250px;
padding: 20px;
overflow: hidden;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s ease-out;
}
.fade-enter {
opacity: 0;
transform: translateX(-50px);
}
.fade-leave-to {
opacity: 0;
transform: translateX(50px);
}
</style>

55
src/layout/home/menu.vue Normal file
View File

@ -0,0 +1,55 @@
<template>
<el-scrollbar>
<div class="menu-title">
患者管理系统
</div>
<el-menu :default-active="route.path" router>
<el-menu-item index="/user">
<el-icon><User /></el-icon>
患者管理
</el-menu-item>
<el-menu-item index="/news">
<el-icon><Document /></el-icon>
健康文章
</el-menu-item>
</el-menu>
</el-scrollbar>
</template>
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute()
</script>
<style lang="scss" scoped>
.el-menu {
border-right: none;
background-color: #2b3a4a;
color: #fff;
}
.el-menu-item {
color: #fff;
&:hover {
background-color: #34495e;
}
&.is-active {
background-color: var(--el-color-primary);
color: #fff;
}
}
.el-scrollbar {
height: 100%;
background-color: #2b3a4a;
}
.menu-title {
font-size: 16px;
font-weight: bold;
text-align: center;
height: 60px;
line-height: 60px;
color: #fff;
background-color: #2b3a4a;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div class="setting">
<el-link type="danger" :underline="false" @click="logout">退出</el-link>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const logout = () => {
localStorage.removeItem('TOKEN')
router.push('/login')
}
</script>
<style lang="scss" scoped>
.setting {
float: right;
}
.dialog-div {
height: 90%;
display: flex;
justify-content: center;
align-items: center;
.login-form {
width: 400px;
padding: 60px 40px;
/* border-radius: 8px; */
/* box-shadow: var(--zuo-shadow); */
/* box-shadow: 0px 0px 30px 0px rgba(0, 0, 0, .04), 0px 30px 60px 0px rgba(0, 0, 0, .12), 0px 0px 1px 0px rgba(0, 0, 0, .3); */
h2 {
font-size: 30px;
text-align: center;
margin-bottom: 50px;
}
:deep(.el-form-item) {
display: block;
}
.email-code {
display: flex;
.el-input {
margin-right: 10px;
}
.code-btn {
width: 200px;
}
}
.login-btn {
width: 100%;
margin-top: 30px;
}
.tip {
line-height: 22px;
}
.block-btn {
display: block;
width: 100%;
margin-top: 30px;
}
input {
width: 100%;
height: 40px;
border: 0;
text-indent: 12px;
border-bottom: 1px solid var(--el-color-primary);
font-size: 16px;
}
}
.login-form {
display: flex;
align-items: center;
justify-content: center;
.login-box {
box-shadow: var(--el-box-shadow);
padding: 50px;
border-radius: var(--my-border-radius-large);
}
p {
margin-top: 10px;
text-align: center;
font-size: 18px;
// font-weight: bold;
}
.qr-box {
width: 240px;
height: 240px;
position: relative;
img {
width: 240px;
position: relative;
z-index: 0;
}
.qr-expire {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba($color: #000000, $alpha: 0.7);
color: #fff;
z-index: 1;
cursor: pointer;
}
}
:deep(.el-loading-spinner) {
transform: translateY(-50%);
}
}
}
</style>

40
src/main.ts Normal file
View File

@ -0,0 +1,40 @@
import { createApp } from 'vue'
import App from './App.vue'
// import 'virtual:svg-icons-register'
import globalComponent from './components/index'
import { router } from './router'
import pinia from './store'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
// default-passive-events 是一个轻量级的 JavaScript 库,旨在通过自动为支持 EventListenerOptions 的浏览器设置事件监听器的 { passive: true } 选项来优化滚动性能。这有助于减少触摸和滚轮事件处理时的延迟,从而提升用户体验。
import 'default-passive-events'
// 修改主题必须文件
import 'element-plus/theme-chalk/src/index.scss'
// 重置样式
import '@/styles/index.scss'
// 暗黑模式
import 'element-plus/theme-chalk/dark/css-vars.css'
// 自定义指令
// import { isHasButton } from '@/directive/has.ts'
// 屏幕自适应
// import './utils/fitter.js'
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn,
})
app.use(globalComponent)
app.use(pinia)
app.use(router)
app.mount('#app')

106
src/router/index.ts Normal file
View File

@ -0,0 +1,106 @@
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
// import Nprogress from 'nprogress'
// import 'nprogress/nprogress.css'
import { GET_TOKEN } from '@/utils/token'
const constantRoutes = [
{
path: '/',
name: 'Layout',
component: () => import('@/layout/home/index.vue'),
redirect: '/user',
meta: {
title: '首页'
},
children: [
{
path: '/user',
name: 'user',
component: () => import('@/views/user/index.vue'),
meta: {
title: '患者管理'
},
},
{
path: '/news',
name: 'news',
component: () => import('@/views/news/index.vue'),
meta: {
title: '文章管理'
},
},
{
path: '/addNew',
name: 'addNew',
component: () => import('@/views/news/addNew.vue'),
meta: {
title: '添加文章'
},
},
],
},
{
path: '/login',
name: 'login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
},
},
{
path: '/:pathMatch(.*)',
name: 'Any',
component: () => import('@/views/404/index.vue'),
meta: {
title: '404',
hidden: true,
icon: '',
},
}
]
export const router = createRouter({
history: createWebHistory(),
routes: constantRoutes,
// 切换路由跳转到顶部
scrollBehavior() {
return {
left: 0,
top: 0
}
}
})
const whiteList = ['/login']
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// Nprogress.start()
const token = GET_TOKEN()
console.log(token)
if (token) {
if (to.path === '/login') {
next('/index')
} else {
next()
}
} else {
if (whiteList.indexOf(to.path) > -1) {
next()
} else {
next('/login')
}
}
})
// 全局后置守卫
router.afterEach(() => {
// Nprogress.done()
})

8
src/store/index.ts Normal file
View File

@ -0,0 +1,8 @@
// 大仓库
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
export default pinia

View File

@ -0,0 +1,13 @@
import { defineStore } from "pinia"
const pinia = defineStore('main', {
state: () => ({
}),
actions: {
}
})
export default pinia

33
src/store/modules/user.ts Normal file
View File

@ -0,0 +1,33 @@
// 用户相关仓库
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { REMOVE_TOKEN } from '@/utils/token'
const useUserStore = defineStore(
'mihoutaoUser',
() => {
const userInfo = ref({})
const token = ref('')
const getUserInfo = async (data: any) => {
userInfo.value = data
}
const userLogout = async () => {
//退出登录请求
REMOVE_TOKEN()
userInfo.value = {}
}
return {
userInfo,
token,
getUserInfo,
userLogout,
}
},
{
persist: true
}
)
export default useUserStore

View File

@ -0,0 +1,11 @@
// styles/element/index.scss
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': #5CC4A7,
),
),
);

111
src/styles/index.scss Normal file
View File

@ -0,0 +1,111 @@
//引入清除默认样式
@import './reset.scss';
:root {
// --my-shadow: 0 1px 3px 0 rgba(0, 0, 0, .1);
// --my-shadow-large: 0px 0px 3px 5px var(--el-border-color);
// --my-shadow-small: 0px 0px 0px 1px var(--el-border-color);
--my-border-radius: 8px;
--my-border-radius-large: 16px;
--my-border-radius-small: 4px;
--el-fill-color-blank: rgba(240, 245, 245, 1) !important;
}
//滚动条外观设置
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-thumb {
width: 10px;
background-color: #c4cbd7;
border-radius: 10px;
}
.el-popover.el-popper {
border-radius: var(--my-border-radius) !important;
}
.el-popper__arrow {
display: none;
}
.el-popper.is-light {
border: 0;
}
.pagination-box {
// overflow: hidden;
margin-top: 25px;
text-align: right;
height: 30px;
.el-pagination {
float: right;
}
}
.el-table {
--el-table-header-text-color: var(--el-color-primary) !important;
--el-table-bg-color: var(--el-bg-color) !important;
}
.el-input {
--el-select-input-focus-border-color: none !important;
--el-input-bg-color: rgba(240, 245, 245, 1) !important;
--el-input-focus-border-color: none;
--el-input-border-color: none !important;
}
.el-input__wrapper.is-focus,
.el-select__wrapper.is-focused,
.el-select__wrapper {
box-shadow: none !important;
}
.el-textarea,
.el-date-editor,
.el-cascader {
--el-input-focus-border-color: none !important;
--el-input-bg-color: rgba(240, 245, 245, 1) !important;
--el-input-border-color: none !important;
}
.el-input__wrapper:hover,
.el-textarea__inner:hover,
.el-input-group__append:hover,
.el-input-group__prepend:hover {
box-shadow: none !important;
}
.el-input__wrapper.is-focus,
.el-textarea__inner:focus,
.el-date-editor.is-active {
box-shadow: none !important;
}
.el-input-group__append,
.el-input-group__prepend {
background: var(--el-input-bg-color) !important;
box-shadow: none !important;
padding: 0 10px !important;
}
.el-textarea,
.el-date-editor,
.el-cascader {
--el-input-focus-border-color: none !important;
--el-input-border-color: none !important;
}
.el-radio{
--el-radio-input-bg-color: #fff!important;
}
.el-descriptions__body {
background-color: #fff!important;
}

202
src/styles/reset.scss Normal file
View File

@ -0,0 +1,202 @@
*,
*:after,
*:before {
box-sizing: border-box;
outline: none;
font-family: Avenir, Helvetica, Arial, sans-serif;
}
html {
height: 100%;
font-size: 16px;
// min-width: 1300px;
}
html,
body,
div,
span,
applet,
object,
iframe,
h1,
h2,
h3,
h4,
h5,
h6,
p,
blockquote,
pre,
a,
abbr,
acronym,
address,
big,
cite,
code,
del,
dfn,
em,
img,
ins,
kbd,
q,
s,
samp,
small,
strike,
strong,
sub,
sup,
tt,
var,
b,
u,
i,
center,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
article,
aside,
canvas,
details,
embed,
figure,
figcaption,
footer,
header,
hgroup,
menu,
nav,
output,
ruby,
section,
summary,
time,
mark,
audio,
video {
// font: inherit;
font-size: 100%;
margin: 0;
padding: 0;
vertical-align: baseline;
border: 0;
}
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
menu,
nav,
section {
display: block;
}
body {
height: 100%;
line-height: 1;
font-size: 16px;
color: var(--el-text-color-regular);
}
ol,
ul {
list-style: none;
}
blockquote,
q {
quotes: none;
&:before,
&:after {
content: '';
content: none;
}
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -.5em;
}
sub {
bottom: -.25em;
}
table {
border-spacing: 0;
border-collapse: collapse;
}
input,
textarea,
button {
// font-family: inhert;
font-family: Avenir, Helvetica, Arial, sans-serif;
font-size: inherit;
color: inherit;
}
select {
text-indent: .01px;
text-overflow: '';
border: 0;
border-radius: 0;
-webkit-appearance: none;
-moz-appearance: none;
}
select::-ms-expand {
display: none;
}
code,
pre {
font-family: monospace, monospace;
font-size: 1em;
}
html.dark {
--el-text-color-regular: #606266;
}

25
src/utils/fitter.js Normal file
View File

@ -0,0 +1,25 @@
// 计算公式当前屏幕宽度winWidth除以设计稿宽度1920得到屏幕与设计稿的比例根据比例设置根元素的fontSize设计稿的100px等于1rem
// 获取屏幕宽度
var winWidth = document.documentElement.offsetWidth || document.body.offsetWidth
// 获取html跟元素
var oHtml = document.getElementsByTagName('html')[0]
// oHtml.style.fontSize = 100 * winWidth / 1680 + 'px'
if(winWidth > 1680){
oHtml.style.fontSize = 100 * 1680 / 1680 + 'px'
} else {
oHtml.style.fontSize = 100 * winWidth / 1680 + 'px'
}
// 页面大小发生变化事重新设置
window.addEventListener('resize', function() {
var winWidth = document.documentElement.offsetWidth || document.body.offsetWidth
var oHtml = document.getElementsByTagName('html')[0]
if(winWidth > 1680){
oHtml.style.fontSize = 100 * 1680 / 1680 + 'px'
} else {
oHtml.style.fontSize = 100 * winWidth / 1680 + 'px'
}
})

55
src/utils/index.ts Normal file
View File

@ -0,0 +1,55 @@
/**
* @description
* @param {Number} min
* @param {Number} max
* @returns {Number}
*/
export function randomNum(min: number, max: number): number {
const num = Math.floor(Math.random() * (min - max) + max)
return num
}
/**
* rgba转#
* @param color
*/
export function getHexColor(color: string) {
const values = color
.replace(/rgba?\(/, '')
.replace(/\)/, '')
.replace(/[\s+]/g, '')
.split(',')
const a = parseFloat(values[3]),
r = Math.floor(a * parseInt(values[0]) + (1 - a) * 255),
g = Math.floor(a * parseInt(values[1]) + (1 - a) * 255),
b = Math.floor(a * parseInt(values[2]) + (1 - a) * 255)
return '#' +
('0' + r.toString(16)).slice(-2) +
('0' + g.toString(16)).slice(-2) +
('0' + b.toString(16)).slice(-2)
}
/**
* Component的方法
* @param view
* @param type
*/
export function _getViews(view: any, type: any) {
let res;
let modules: any;
if (type == "one") {
modules = import.meta.glob("/src/view/*.vue");
} else {
modules = import.meta.glob("/src/view/**/*.vue");
}
for (const path in modules) {
const dir =
type == "one"
? path.split("view/")[1].split(".vue")[0]
: path.split("view/")[1].split(".vue")[0];
if (dir === view) {
res = () => modules[path]();
}
}
return res;
};

68
src/utils/request.ts Normal file
View File

@ -0,0 +1,68 @@
import axios from 'axios'
import { ElNotification } from 'element-plus'
import { GET_TOKEN, REMOVE_TOKEN } from '@/utils/token'
import useUserStore from '@/store/modules/user'
import pinia from '@/store'
//创建axios实例
const request = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 120000,
})
// 存储需要取消的请求
const requestsToCancel = {};
//请求拦截器
request.interceptors.request.use((config: any) => {
if (GET_TOKEN()) {
config.headers.Authorization = GET_TOKEN()
}
// 为每个请求生成一个唯一的cancel token
config.cancelToken = new axios.CancelToken(cancel => {
requestsToCancel[config.url] = cancel;
});
return config
})
//响应拦截器
request.interceptors.response.use(
(response: any) => {
return response.data
},
(error: any) => {
//处理网络错误
if (error.response?.data.code == 20107) {
return
}
// if (error.response?.data.code == 10101) {
// Object.values(requestsToCancel).forEach(cancel => cancel());
// }
if (error.response?.data.code == 10103) {
Object.values(requestsToCancel).forEach(cancel => cancel());
const useUser = useUserStore(pinia)
useUser.userInfo = {}
useUser.token = ''
REMOVE_TOKEN()
ElNotification({
type: 'error',
message: error.response.data.message,
duration: 2000,
onClose: () => {
if(location.pathname != '/index'){
location.href = '/index'
}
}
})
return
}
ElNotification({
type: 'error',
message: error.response.data.message,
duration: 2000,
})
return Promise.reject(error)
},
)
export default request

12
src/utils/time.ts Normal file
View File

@ -0,0 +1,12 @@
export const getTime = () => {
const hours = new Date().getHours()
if (hours <= 9) {
return '早上好 🌅'
} else if (hours <= 12) {
return '上午好 🌞'
} else if (hours <= 18) {
return '下午好 ☕️'
} else {
return '晚上好 🌛'
}
}

18
src/utils/token.ts Normal file
View File

@ -0,0 +1,18 @@
export const SET_TOKEN = (token: string) => {
localStorage.setItem('TOKEN', token)
}
export const GET_TOKEN = () => {
return localStorage.getItem('TOKEN')
}
export const REMOVE_TOKEN = () => {
localStorage.removeItem('TOKEN')
}
export const SET_USER_TYPE = (type) => {
localStorage.removeItem('userType')
}

42
src/utils/validate.ts Normal file
View File

@ -0,0 +1,42 @@
// 电话校验
export const checkPhoneNumber = (rule: any, value: any, callback: any) => {
const reg = /^[1][3456789][0-9]{9}$/
if ((reg.test(value))) {
callback()
} else {
callback(new Error('validate.please_enter_the_correct_phone_number'))
}
}
// 验证邮箱的规则
export const checkEmail = (rule: any, value: any, callback: any) => {
// 验证邮箱的正则表达式
const reg = /^[a-zA-Z0-9]+([\.-_][a-zA-Z0-9]+)*@[a-zA-Z0-9]+([\.-][a-zA-Z0-9]+)*\.[a-zA-Z]{2,}$/
if (reg.test(value)) {
callback()
}
callback(new Error('请输入正确的的邮箱'))
}
// 密码规则
export const checkPassword= (rule: any, value: any, callback: any) => {
const reg = /^[a-zA-Z0-9\W]{6,10}$/
if (reg.test(value)) {
callback()
}
callback(new Error('你输入6到10位密码, 不能包含特殊符号'))
}
// 数字验证
export const checkNumber = (rule: any, value: any, callback: any) => {
if(isNaN(value)){
callback(new Error('validate.please_enter_a_number_greater_than_0'))
} else {
if (value > 0) {
callback()
} else {
callback(new Error('validate.please_enter_a_number_greater_than_0'))
}
}
}

52
src/views/404/index.vue Normal file
View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import { useRouter } from 'vue-router';
let $router = useRouter();
const goHome = () => {
$router.push('/index')
}
</script>
<template>
<div class="box">
<img src="../../assets/images/error_images/404.png" />
<div>
<h2>您访问的页面不存在</h2>
<p>请检查您的地址是否正确或者点击下面按钮回到首页</p>
<div>
<el-button type="primary" round @click="goHome">返回首页</el-button>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.box {
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
img {
width: 800px;
height: 400px;
}
&>div {
display: flex;
flex-direction: column;
h2 {
color: var(--el-color-primary);
font-size: 36px;
font-weight: 600;
}
p {
color: gray;
font-size: 16px;
margin: 20px 0 50px 0;
}
}
}
</style>

View File

@ -0,0 +1,769 @@
<template>
<div class="container">
<div class="ai-box">
<div class="editor-box">
<div class="toolbar-box">
<Toolbar style="" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
</div>
<div class="title-box">
<input type="text" v-model="newTitle" class="title-input" placeholder="请输入文章标题" />
</div>
<Editor class="editor" style="height: calc(100% - 190px); overflow-y: hidden;" v-model="valueHtml"
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
<!-- 添加字数统计显示 -->
<div class="editor-btn-box">
<div class="character-count">
字数: {{ characterCount }}
</div>
<el-button type="primary" @click="handleSaveArticle(1)">保存</el-button>
<el-button type="primary" @click="handleSaveArticle(2)">存草稿</el-button>
<el-button type="primary" @click="handlePreviewArticle">预览</el-button>
</div>
</div>
</div>
<el-dialog v-model="dialogPreview" title="文章预览" width="1000px">
<div class="dialog-preview" style="padding: 20px;">
<div class="preview-title">{{ newTitle }}</div>
<div class="preview-content" v-html="valueHtml">
</div>
</div>
<template #footer>
<el-button type="primary" @click="dialogPreview = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // css
import { Loading } from '@element-plus/icons-vue'
import { onBeforeUnmount, onUnmounted , ref, reactive, shallowRef, onMounted, computed, getCurrentInstance, nextTick } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { title, inspiration, configList, writeOutline, models, generateArticle, analysisStyle, saveStyle, saveArticle, editArticle, customerTool } from '@/api/AICreation'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const generating = ref(1)
// shallowRef
const editorRef = shallowRef()
// HTML
const valueHtml = ref('')
const mode = ref('default')
//
const valueText = ref('')
//
const characterCount = ref(0)
const toolbarConfig = {}
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {}, // Initialize MENU_CONF as an empty object
}
editorConfig.MENU_CONF['uploadImage'] = {
//
server: import.meta.env.VITE_API_BASE_URL + '/api/upload', // 使APIURL
// 10M
maxFileSize: 5 * 1024 * 1024, // 5M
//
allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
//
meta: {
token: localStorage.getItem('token') || ''
},
// http
headers: {
Authorization: 'Bearer ' + (localStorage.getItem('token') || '')
},
//
onBeforeUpload(file) {
console.log('准备上传图片', file)
return file // file
},
//
onSuccess(file, res) {
console.log('图片上传成功', file, res)
},
//
onError(file, err, res) {
console.log('图片上传失败', file, err, res)
//
ElNotification.error('图片上传失败,请重试')
},
//
customInsert(res, insertFn) {
// res
if (res.code === 200 && res.data) {
// URL
const url = res.data.url || res.data
//
insertFn(url)
} else {
ElNotification.error('图片插入失败')
}
}
}
editorConfig.MENU_CONF['uploadVideo'] = {
server: import.meta.env.VITE_API_BASE_URL + '/api/upload', // 使APIURL
maxFileSize: 5 * 1024 * 1024, // 5M
onBeforeUpload: (file) => {
console.log('准备上传视频', file)
return file // file
},
onSuccess: (file, res) => {
console.log('视频上传成功', file, res)
},
onError: (file, err, res) => {
console.log('视频上传失败', file, err, res)
ElNotification.error('视频上传失败,请重试')
},
}
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // editor
if (route.query.id) {
editor.setHtml(sessionStorage.getItem('works'))
// valueHtml.value = sessionStorage.getItem('works')
}
//
editor.on('change', (e) => {
//
valueText.value = editor.getText()
//
characterCount.value = valueText.value.length
})
}
const newTitle = ref('')
onMounted(() => {
})
// Auto-resize directive
const vAutoResize = {
mounted: (el) => {
const resize = () => {
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}
// Initial resize
resize()
// Add event listeners
el.addEventListener('input', resize)
window.addEventListener('resize', resize)
// Cleanup
onBeforeUnmount(() => {
el.removeEventListener('input', resize)
window.removeEventListener('resize', resize)
})
}
}
// Register directive
const app = getCurrentInstance()
app.appContext.app.directive('auto-resize', vAutoResize)
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
.toolbar-box{
height: 80px;
}
:deep(.el-radio__label) {
white-space: normal;
}
.title-input {
width: 100%;
height: 50px;
border: none;
// margin: 20px 0;
font-size: 20px;
}
.ai-box {
width: 100%;
height: 100%;
display: flex;
.editor-box {
height: 100%;
flex: 1;
padding-right: 60px;
position: relative;
&::after {
content: '';
width: 1px;
height: 100%;
background-color: var(--el-border-color);
position: absolute;
right: 30px;
top: 0;
}
.editor {
height: calc(100% - 300px);
}
.editor-btn-box {
width: 100%;
text-align: center;
margin-top: 15px;
position: relative;
}
.character-count {
position: absolute;
right: 0;
bottom: 15px;
text-align: right;
color: #909399;
font-size: 12px;
margin-top: 5px;
padding-right: 10px;
}
.title-box {
display: flex;
border-bottom: 1px solid var(--el-border-color);
margin-bottom: 10px;
.title-input {
flex: 1;
}
.title-btn {
flex: none;
width: 100px;
}
}
}
.active-box {
width: 350px;
flex: none;
overflow: hidden;
.el-scrollbar {
overflow-x: hidden;
}
}
}
.tab-item {
margin-top: 10px;
margin-bottom: 15px;
.tab-item-label {
margin-top: 15px;
font-size: 14px;
margin-bottom: 15px;
}
}
.tab-item-btn {
// margin-top: 15px;
}
.tab-item-radio-box {
padding: 20px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
margin-bottom: 15px;
display: flex;
height: 160px;
overflow-y: auto;
:deep(.el-radio-group) {
display: block;
overflow: auto;
}
:deep(.el-radio) {
display: flex;
height: auto;
line-height: 18px;
margin: 5px 0;
}
}
.tab-item-label {
display: block;
margin-top: 15px;
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
}
.model-label {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
}
.model-box {
margin-top: 15px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
padding: 15px 20px;
div {
height: 30px;
line-height: 30px;
// border: 1px solid var(--el-border-color);
cursor: pointer;
// border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
// margin-bottom: 10px;
span {
color: rgb(239, 53, 53);
font-size: 14px;
margin-left: 15px;
font-style: italic;
vertical-align: middle;
.el-icon {
font-size: 28px;
position: relative;
top: 6px;
}
}
&:hover {
color: var(--el-color-warning);
}
}
.active {
color: var(--el-color-warning);
}
}
.model-item {
padding: 15px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
cursor: pointer;
label {
font-size: 15px;
font-weight: 600;
line-height: 24px;
}
p {
font-size: 13px;
line-height: 24px;
}
&.active {
background-color: var(--el-color-primary);
label {
color: #fff;
}
p {
color: #fff;
}
}
}
.tab-item-select-box {
.el-select {
margin-bottom: 15px;
}
}
.tab-item-text {
margin: 15px 0;
}
.dialog-content {
margin: 20px;
}
.dialog-title {
margin: 20px 0;
}
.dialog-content-title {
// height: 200px;
min-height: 200px;
border-radius: 6px;
background-color: var(--el-fill-color-blank);
margin-bottom: 20px;
padding: 20px 15px;
p {
line-height: 20px;
padding-right: 50px;
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.dialog-content-title-span {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
.drawer-title {
display: flex;
justify-content: center;
.serial-number {
height: 30px;
width: 30px;
border-radius: 30px;
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary-light-5);
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.drawer-title-text {
line-height: 30px;
font-size: 14px;
margin-right: 15px;
color: var(--el-color-primary-light-5);
}
.steps-item {
&::after {
border-radius: 50%;
box-shadow: -10px -8px 0 -2px var(--el-color-primary), 0 -8px 0 -1px var(--el-color-primary-light-5), 10px -8px 0 -2px var(--el-color-primary-light-5);
content: " ";
display: inline-block;
height: 8px;
margin: 0 10px;
position: relative;
transform: translateY(calc(100% - 1px)) translateX(8px);
width: 8px;
}
}
.active {
color: var(--el-color-primary);
}
}
:deep(.el-drawer__header) {
margin-bottom: 0;
}
:deep(.el-drawer__body) {
overflow: hidden;
padding: 0;
}
.drawer-content, .drawer-content2 {
padding: 20px;
padding-top: 0;
margin-top: 20px;
height: calc(100vh - 142px);
overflow-y: auto;
.drawer-content-item {
margin-bottom: 20px;
transition: all 0.3s;
.drawer-content-item-button {
text-align: right;
margin-top: 5px;
}
.expand-enter-active,
.expand-leave-active {
transition: all 0.3s ease-in-out;
max-height: 50px;
/* 根据实际内容调整最大高度 */
overflow: hidden;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
.drawer-content-item-box {
border-radius: 6px;
background-color: #f9f9fb;
padding: 18px;
padding-top: 10px;
padding-bottom: 15px;
position: relative;
&.streaming {
background-color: rgba(var(--el-color-primary-rgb), 0.05);
border: 1px solid rgba(var(--el-color-primary-rgb), 0.1);
}
.streaming-cursor {
position: absolute;
right: 18px;
bottom: 15px;
width: 8px;
height: 16px;
background-color: var(--el-color-primary);
animation: blink 1s infinite;
}
}
.EaFdC {
// list-style: cjk-ideographic;
margin-bottom: 4px;
}
.drawer-content-title {
font-size: 16px;
line-height: 24px;
font-weight: 500;
line-height: 24px;
// list-style: cjk-ideographic;
margin: 0;
// margin-left: 35px;
height: auto;
min-height: 24px;
&::marker {
unicode-bidi: isolate;
font-variant-numeric: tabular-nums;
text-transform: none;
text-indent: 0px !important;
text-align: start !important;
text-align-last: start !important;
transform: translateY(-3px);
}
textarea {
width: 100%;
background-color: rgba($color: #000000, $alpha: 0);
border: 0;
font-style: normal;
resize: none;
min-height: 24px;
overflow: hidden;
position: relative;
top: 7px;
padding: 0;
}
}
.drawer-content-text {
font-size: 14px;
line-height: 20px;
width: 100%;
background-color: rgba($color: #000000, $alpha: 0);
border: 0;
font-style: normal;
resize: none;
height: auto;
min-height: 24px;
overflow: hidden;
padding: 0;
word-break: break-all;
}
}
}
.generating-indicator {
position: absolute;
top: 70px;
left: 30px;
color: var(--el-color-primary);
font-size: 28px;
font-weight: bold;
}
.loading-dots {
animation: loadingDots 1.5s infinite;
}
@keyframes loadingDots {
0% { opacity: 0.2; }
20% { opacity: 1; }
100% { opacity: 0.2; }
}
.article-preview {
padding: 10px;
height: 100%;
.article-preview-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: var(--el-color-primary);
}
.article-preview-content {
font-size: 14px;
line-height: 1.6;
// max-height: calc(100vh - 200px);
overflow-y: auto;
padding: 15px;
background-color: #f9f9fb;
border-radius: 6px;
h1,
h2,
h3 {
margin-top: 16px;
margin-bottom: 8px;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
p {
margin-bottom: 10px;
}
}
}
.extract-style {
font-size: 14px;
background-color: var(--el-fill-color-blank);
border-radius: 6px;
padding: 15px;
// min-height: 100px;
// max-height: 200px;
overflow-y: auto;
height: 200px;
display: flex;
pre {
line-height: 18px;
white-space: pre-wrap;
}
}
.no-click {
pointer-events: none;
}
.article-preview-title {
font-size: 18px;
margin-bottom: 15px;
}
.dialog-preview {
max-height: calc(100vh - 200px);
overflow: auto;
.preview-title {
font-size: 24px;
color: var(--el-text-color-primary);
margin-bottom: 40px;
}
.preview-content {
color: var(--el-text-color-primary);
font-size: 16px;
line-height: 1;
// p{
// line-height: 26px;
// }
}
}
.drawer-footer{
display: flex;
justify-content: space-between;
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<div>
<div class="hot-search">
<div class="hot-search-type">
<el-button :type="activeType === 'ai' ? 'primary' : 'default'" @click="handleFilter('ai')">AI
热榜</el-button>
<el-button :type="activeType === 'low' ? 'primary' : 'default'"
@click="handleFilter('low')">低粉高爆</el-button>
<el-button :type="activeType === 'create' ? 'primary' : 'default'"
@click="handleFilter('create')">创作榜单</el-button>
</div>
<div class="hot-search-filter">
<span>关键词</span>
<el-input v-model="query.keyword" placeholder="关键词搜索" style="width: 200px;" />
<span>阅读量</span>
<el-select v-model="query.readCount" placeholder="请选择阅读量" style="width: 200px;">
<el-option label="1000-10000" value="1000-10000"></el-option>
<el-option label="10000-100000" value="10000-100000"></el-option>
<el-option label="100000-1000000" value="100000-1000000"></el-option>
</el-select>
<span>发布时间</span>
<el-date-picker style="width: 300px;" v-model="date" type="daterange" range-separator=""
start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD"
@change="handleDateChange" />
<el-button type="primary" @click="handleFilter">搜索</el-button>
</div>
</div>
<el-table :data="tableData" style="width: 100%;">
<el-table-column label="序号" type="index" width="80" />
<el-table-column prop="title" label="标题" />
<el-table-column prop="" label="领域" />
<el-table-column prop="readCount" label="阅读量" />
<el-table-column prop="publishTime" label="发布时间" />
<el-table-column prop="action" label="操作" width="150">
<template #default="scope">
<el-link type="primary" @click="handleEdit(scope.row)">AI 改写</el-link>
<el-link type="primary" @click="handleEdit(scope.row)">查看原文</el-link>
<el-link type="primary" @click="handleEdit(scope.row)">创作灵感</el-link>
</template>
</el-table-column>
</el-table>
<el-pagination background v-model:current-page="query.page" v-model:page-size="query.pageSize"
:page-sizes="[100, 200, 300, 400]" :size="query.pageSize" :disabled="disabled" :background="background"
layout="total, sizes, prev, pager, next, jumper" :total="400" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
const activeType = ref('ai');
const handleFilter = (type) => {
activeType.value = type;
};
const query = reactive({
page: 1,
pageSize: 10,
keyword: '',
readCount: '',
startDate: '',
endDate: '',
});
const handleDateChange = (value) => {
query.startDate = value[0];
query.endDate = value[1];
};
const handleSizeChange = (size) => {
pageSize.value = size;
};
const handleCurrentChange = (page) => {
query.page = page;
};
onMounted(() => {
handleFilter(activeType.value);
});
</script>
<style scoped lang="scss">
.hot-search {
.hot-search-type {
margin-bottom: 20px;
}
.hot-search-filter {
line-height: 36px;
span {
margin-right: 10px;
font-size: 14px;
}
.el-input {
margin-right: 10px;
}
.el-select {
margin-right: 10px;
}
.el-button {
margin-left: 10px;
}
}
}
.el-table {
margin-top: 20px;
}
.el-pagination {
margin-top: 20px;
}
.el-link+ .el-link {
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,8 @@
<template>
<div>
<h1>热榜</h1>
</div>
</template>
<script setup>
</script>

63
src/views/home/index.vue Normal file
View File

@ -0,0 +1,63 @@
<script setup lang="ts">
import { getTime } from '@/utils/time';
//,
import useUserStore from '@/store/modules/user';
//
let userStore = useUserStore();
</script>
<template>
<div class="container">
<el-card>
<div class="box">
<div class="bottom">
<!-- <h3 class="title">{{ getTime() }} {{ userStore.username }} </h3> -->
<p class="subtitle">{{ $t('common.welcome_to_use') }} {{ $t('common.carbonease') }} CarbonEasy</p>
</div>
</div>
</el-card>
<div class="bottoms">
</div>
</div>
</template>
<style scoped lang="scss">
.container {
height: 100%;
background: url(../../assets/images/welcome.png) no-repeat center 60%;
background-size: 70%;
}
.box {
display: flex;
.avatar {
width: 100px;
height: 100px;
border-radius: 50%;
}
.bottom {
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 30px;
.title {
font-size: 30px;
font-weight: 900;
margin-bottom: 30px;
}
.subtitle {
font-style: italic;
}
}
}
.bottoms{
margin-top: 10px;
display: flex;
justify-content: center;
}
</style>

88
src/views/index/index.vue Normal file
View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { GET_TOKEN } from '@/utils/token';
import { useRouter } from 'vue-router';
import { accountImages } from '@/api/group';
import emitter from '@/eventBus'
import Header from '@/layout/header/index.vue'
const router = useRouter()
const imageList = ref([])
const toDash = () => {
if (GET_TOKEN()) {
router.push('/dash')
} else {
emitter.emit('userLogin', 1)
}
}
const getAccoutImages = async () => {
const res = await accountImages()
imageList.value = res.list
}
onMounted(() => {
getAccoutImages()
})
</script>
<template>
<Header />
<div class="container">
<p>一站式公众号订阅</p>
<div class="start-use">
<button @click="toDash">开始使用</button>
</div>
<div class="account-image">
<el-row>
<el-col :span="4" v-for="item in imageList"><img :src="item.hd_head_img" /></el-col>
</el-row>
</div>
</div>
</template>
<style scoped lang="scss">
.container {
height: 100%;
padding-top: 200px;
background-color: #fff;
border-radius: 18px;
overflow: auto;
text-align: center;
font-size: 60px;
font-weight: bold;
.start-use {
text-align: center;
margin-top: 50px;
button{
width: 220px;
height: 60px;
color: #fff;
font-size: 22px;
background-color: var(--el-color-primary);
border: 0;
cursor: pointer;
border-radius: var(--el-border-radius-base);
&:hover{
background-color: var(--el-color-primary-light-3);
}
}
}
.account-image{
width: 75%;
margin: 100px auto;
img{
display: block;
width: 110px;
margin: auto;
margin-top: 50px;
border-radius: 100px;
box-shadow: var(--el-box-shadow);
}
}
}
</style>

112
src/views/login/index.vue Normal file
View File

@ -0,0 +1,112 @@
<template>
<div class="login-container">
<el-card class="login-card">
<template #header>
<h2 class="login-title">系统登录</h2>
</template>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="loginRules"
label-width="0"
size="large"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
prefix-icon="Lock"
show-password
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="loading"
class="login-button"
@click="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const loginFormRef = ref(null)
const loading = ref(false)
const loginForm = reactive({
username: '',
password: ''
})
const loginRules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
]
}
const handleLogin = async () => {
if (!loginFormRef.value) return
try {
await loginFormRef.value.validate()
loading.value = true
// TODO: API
// const res = await login(loginForm)
localStorage.setItem('TOKEN', '123456')
//
setTimeout(() => {
ElMessage.success('登录成功')
router.push('/')
loading.value = false
}, 1000)
} catch (error) {
ElMessage.error('登录失败,请检查用户名和密码')
loading.value = false
}
}
</script>
<style scoped>
.login-container {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: #f5f5f5;
}
.login-card {
width: 400px;
}
.login-title {
text-align: center;
margin: 0;
color: #303133;
}
.login-button {
width: 100%;
}
</style>

760
src/views/news/addNew.vue Normal file
View File

@ -0,0 +1,760 @@
<template>
<div class="container">
<div class="ai-box">
<div class="editor-box">
<div class="toolbar-box">
<Toolbar style="" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" />
</div>
<div class="title-box">
<input type="text" v-model="newTitle" class="title-input" placeholder="请输入文章标题" />
</div>
<Editor class="editor" style="height: calc(100% - 190px); overflow-y: hidden;" v-model="valueHtml"
:defaultConfig="editorConfig" :mode="mode" @onCreated="handleCreated" />
<!-- 添加字数统计显示 -->
<div class="editor-btn-box">
<div class="character-count">
字数: {{ characterCount }}
</div>
<el-button type="primary" @click="handleSaveArticle(1)">保存</el-button>
<el-button type="primary" @click="handleSaveArticle(2)">存草稿</el-button>
<el-button type="primary" @click="handlePreviewArticle">预览</el-button>
</div>
</div>
</div>
<el-dialog v-model="dialogPreview" title="文章预览" width="1000px">
<div class="dialog-preview" style="padding: 20px;">
<div class="preview-title">{{ newTitle }}</div>
<div class="preview-content" v-html="valueHtml">
</div>
</div>
<template #footer>
<el-button type="primary" @click="dialogPreview = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import '@wangeditor/editor/dist/css/style.css' // css
import { Loading } from '@element-plus/icons-vue'
import { onBeforeUnmount, onUnmounted , ref, reactive, shallowRef, onMounted, computed, getCurrentInstance, nextTick } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
// import { title, inspiration, configList, writeOutline, models, generateArticle, analysisStyle, saveStyle, saveArticle, editArticle, customerTool } from '@/api/AICreation'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const generating = ref(1)
// shallowRef
const editorRef = shallowRef()
// HTML
const valueHtml = ref('')
const mode = ref('default')
//
const valueText = ref('')
//
const characterCount = ref(0)
const toolbarConfig = {}
const editorConfig = {
placeholder: '请输入内容...',
MENU_CONF: {}, // Initialize MENU_CONF as an empty object
}
editorConfig.MENU_CONF['uploadImage'] = {
//
server: import.meta.env.VITE_API_BASE_URL + '/api/upload', // 使APIURL
// 10M
maxFileSize: 5 * 1024 * 1024, // 5M
//
allowedFileTypes: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
//
meta: {
token: localStorage.getItem('token') || ''
},
// http
headers: {
Authorization: 'Bearer ' + (localStorage.getItem('token') || '')
},
//
onBeforeUpload(file) {
console.log('准备上传图片', file)
return file // file
},
//
onSuccess(file, res) {
console.log('图片上传成功', file, res)
},
//
onError(file, err, res) {
console.log('图片上传失败', file, err, res)
//
ElNotification.error('图片上传失败,请重试')
},
//
customInsert(res, insertFn) {
// res
if (res.code === 200 && res.data) {
// URL
const url = res.data.url || res.data
//
insertFn(url)
} else {
ElNotification.error('图片插入失败')
}
}
}
editorConfig.MENU_CONF['uploadVideo'] = {
server: import.meta.env.VITE_API_BASE_URL + '/api/upload', // 使APIURL
maxFileSize: 5 * 1024 * 1024, // 5M
onBeforeUpload: (file) => {
console.log('准备上传视频', file)
return file // file
},
onSuccess: (file, res) => {
console.log('视频上传成功', file, res)
},
onError: (file, err, res) => {
console.log('视频上传失败', file, err, res)
ElNotification.error('视频上传失败,请重试')
},
}
//
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // editor
if (route.query.id) {
editor.setHtml(sessionStorage.getItem('works'))
// valueHtml.value = sessionStorage.getItem('works')
}
//
editor.on('change', (e) => {
//
valueText.value = editor.getText()
//
characterCount.value = valueText.value.length
})
}
const newTitle = ref('')
onMounted(() => {
})
// Auto-resize directive
const vAutoResize = {
mounted: (el) => {
const resize = () => {
el.style.height = 'auto'
el.style.height = `${el.scrollHeight}px`
}
// Initial resize
resize()
// Add event listeners
el.addEventListener('input', resize)
window.addEventListener('resize', resize)
// Cleanup
onBeforeUnmount(() => {
el.removeEventListener('input', resize)
window.removeEventListener('resize', resize)
})
}
}
// Register directive
const app = getCurrentInstance()
app.appContext.app.directive('auto-resize', vAutoResize)
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
.toolbar-box{
height: 80px;
}
:deep(.el-radio__label) {
white-space: normal;
}
.title-input {
width: 100%;
height: 50px;
border: none;
// margin: 20px 0;
font-size: 20px;
}
.ai-box {
width: 100%;
height: 100%;
display: flex;
.editor-box {
height: 100%;
flex: 1;
position: relative;
.editor {
height: calc(100% - 300px);
}
.editor-btn-box {
width: 100%;
text-align: center;
margin-top: 15px;
position: relative;
}
.character-count {
position: absolute;
right: 0;
bottom: 15px;
text-align: right;
color: #909399;
font-size: 12px;
margin-top: 5px;
padding-right: 10px;
}
.title-box {
display: flex;
border-bottom: 1px solid var(--el-border-color);
margin-bottom: 10px;
.title-input {
flex: 1;
}
.title-btn {
flex: none;
width: 100px;
}
}
}
.active-box {
width: 350px;
flex: none;
overflow: hidden;
.el-scrollbar {
overflow-x: hidden;
}
}
}
.tab-item {
margin-top: 10px;
margin-bottom: 15px;
.tab-item-label {
margin-top: 15px;
font-size: 14px;
margin-bottom: 15px;
}
}
.tab-item-btn {
// margin-top: 15px;
}
.tab-item-radio-box {
padding: 20px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
margin-bottom: 15px;
display: flex;
height: 160px;
overflow-y: auto;
:deep(.el-radio-group) {
display: block;
overflow: auto;
}
:deep(.el-radio) {
display: flex;
height: auto;
line-height: 18px;
margin: 5px 0;
}
}
.tab-item-label {
display: block;
margin-top: 15px;
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
}
.model-label {
display: flex;
justify-content: space-between;
margin-top: 15px;
font-size: 14px;
font-weight: 600;
margin-bottom: 15px;
}
.model-box {
margin-top: 15px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
padding: 15px 20px;
div {
height: 30px;
line-height: 30px;
// border: 1px solid var(--el-border-color);
cursor: pointer;
// border-radius: 4px;
font-size: 14px;
display: flex;
align-items: center;
// margin-bottom: 10px;
span {
color: rgb(239, 53, 53);
font-size: 14px;
margin-left: 15px;
font-style: italic;
vertical-align: middle;
.el-icon {
font-size: 28px;
position: relative;
top: 6px;
}
}
&:hover {
color: var(--el-color-warning);
}
}
.active {
color: var(--el-color-warning);
}
}
.model-item {
padding: 15px;
border-radius: 4px;
background-color: var(--el-fill-color-blank);
cursor: pointer;
label {
font-size: 15px;
font-weight: 600;
line-height: 24px;
}
p {
font-size: 13px;
line-height: 24px;
}
&.active {
background-color: var(--el-color-primary);
label {
color: #fff;
}
p {
color: #fff;
}
}
}
.tab-item-select-box {
.el-select {
margin-bottom: 15px;
}
}
.tab-item-text {
margin: 15px 0;
}
.dialog-content {
margin: 20px;
}
.dialog-title {
margin: 20px 0;
}
.dialog-content-title {
// height: 200px;
min-height: 200px;
border-radius: 6px;
background-color: var(--el-fill-color-blank);
margin-bottom: 20px;
padding: 20px 15px;
p {
line-height: 20px;
padding-right: 50px;
position: relative;
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
.dialog-content-title-span {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
}
.drawer-title {
display: flex;
justify-content: center;
.serial-number {
height: 30px;
width: 30px;
border-radius: 30px;
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary-light-5);
display: flex;
justify-content: center;
align-items: center;
margin-right: 15px;
}
.drawer-title-text {
line-height: 30px;
font-size: 14px;
margin-right: 15px;
color: var(--el-color-primary-light-5);
}
.steps-item {
&::after {
border-radius: 50%;
box-shadow: -10px -8px 0 -2px var(--el-color-primary), 0 -8px 0 -1px var(--el-color-primary-light-5), 10px -8px 0 -2px var(--el-color-primary-light-5);
content: " ";
display: inline-block;
height: 8px;
margin: 0 10px;
position: relative;
transform: translateY(calc(100% - 1px)) translateX(8px);
width: 8px;
}
}
.active {
color: var(--el-color-primary);
}
}
:deep(.el-drawer__header) {
margin-bottom: 0;
}
:deep(.el-drawer__body) {
overflow: hidden;
padding: 0;
}
.drawer-content, .drawer-content2 {
padding: 20px;
padding-top: 0;
margin-top: 20px;
height: calc(100vh - 142px);
overflow-y: auto;
.drawer-content-item {
margin-bottom: 20px;
transition: all 0.3s;
.drawer-content-item-button {
text-align: right;
margin-top: 5px;
}
.expand-enter-active,
.expand-leave-active {
transition: all 0.3s ease-in-out;
max-height: 50px;
/* 根据实际内容调整最大高度 */
overflow: hidden;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
.drawer-content-item-box {
border-radius: 6px;
background-color: #f9f9fb;
padding: 18px;
padding-top: 10px;
padding-bottom: 15px;
position: relative;
&.streaming {
background-color: rgba(var(--el-color-primary-rgb), 0.05);
border: 1px solid rgba(var(--el-color-primary-rgb), 0.1);
}
.streaming-cursor {
position: absolute;
right: 18px;
bottom: 15px;
width: 8px;
height: 16px;
background-color: var(--el-color-primary);
animation: blink 1s infinite;
}
}
.EaFdC {
// list-style: cjk-ideographic;
margin-bottom: 4px;
}
.drawer-content-title {
font-size: 16px;
line-height: 24px;
font-weight: 500;
line-height: 24px;
// list-style: cjk-ideographic;
margin: 0;
// margin-left: 35px;
height: auto;
min-height: 24px;
&::marker {
unicode-bidi: isolate;
font-variant-numeric: tabular-nums;
text-transform: none;
text-indent: 0px !important;
text-align: start !important;
text-align-last: start !important;
transform: translateY(-3px);
}
textarea {
width: 100%;
background-color: rgba($color: #000000, $alpha: 0);
border: 0;
font-style: normal;
resize: none;
min-height: 24px;
overflow: hidden;
position: relative;
top: 7px;
padding: 0;
}
}
.drawer-content-text {
font-size: 14px;
line-height: 20px;
width: 100%;
background-color: rgba($color: #000000, $alpha: 0);
border: 0;
font-style: normal;
resize: none;
height: auto;
min-height: 24px;
overflow: hidden;
padding: 0;
word-break: break-all;
}
}
}
.generating-indicator {
position: absolute;
top: 70px;
left: 30px;
color: var(--el-color-primary);
font-size: 28px;
font-weight: bold;
}
.loading-dots {
animation: loadingDots 1.5s infinite;
}
@keyframes loadingDots {
0% { opacity: 0.2; }
20% { opacity: 1; }
100% { opacity: 0.2; }
}
.article-preview {
padding: 10px;
height: 100%;
.article-preview-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: var(--el-color-primary);
}
.article-preview-content {
font-size: 14px;
line-height: 1.6;
// max-height: calc(100vh - 200px);
overflow-y: auto;
padding: 15px;
background-color: #f9f9fb;
border-radius: 6px;
h1,
h2,
h3 {
margin-top: 16px;
margin-bottom: 8px;
font-weight: 600;
}
h1 {
font-size: 20px;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
p {
margin-bottom: 10px;
}
}
}
.extract-style {
font-size: 14px;
background-color: var(--el-fill-color-blank);
border-radius: 6px;
padding: 15px;
// min-height: 100px;
// max-height: 200px;
overflow-y: auto;
height: 200px;
display: flex;
pre {
line-height: 18px;
white-space: pre-wrap;
}
}
.no-click {
pointer-events: none;
}
.article-preview-title {
font-size: 18px;
margin-bottom: 15px;
}
.dialog-preview {
max-height: calc(100vh - 200px);
overflow: auto;
.preview-title {
font-size: 24px;
color: var(--el-text-color-primary);
margin-bottom: 40px;
}
.preview-content {
color: var(--el-text-color-primary);
font-size: 16px;
line-height: 1;
// p{
// line-height: 26px;
// }
}
}
.drawer-footer{
display: flex;
justify-content: space-between;
}
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>

48
src/views/news/index.vue Normal file
View File

@ -0,0 +1,48 @@
<template>
<div>
<div class="user-search">
<el-input style="width: 200px;margin-right: 10px;" v-model="search" placeholder="请输入文章标题" />
<el-button type="primary">搜索</el-button>
<el-button style="float: right;" type="primary" @click="addNews">添加文章</el-button>
</div>
<div class="user-table">
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="title" label="文章标题" />
<el-table-column prop="author" label="作者" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column prop="updateTime" label="更新时间" />
</el-table>
<el-pagination
background
layout="prev, pager, next"
:total="1000"
:page-size="20"
:current-page="1"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const router = useRouter()
const tableData = ref([
{
title: '文章标题',
author: '张三',
createTime: '2021-01-01',
updateTime: '2021-01-01',
},
])
const search = ref('')
const addNews = () => {
router.push('/addNew')
}
</script>
<style scoped>
.user-search {
margin-bottom: 10px;
}
</style>

40
src/views/user/index.vue Normal file
View File

@ -0,0 +1,40 @@
<template>
<div>
<div class="user-search">
<el-input style="width: 200px;margin-right: 10px;" v-model="search" placeholder="请输入患者姓名" />
<el-button type="primary">搜索</el-button>
</div>
<div class="user-table">
<el-table :data="tableData" style="width: 100%">
<el-table-column prop="name" label="姓名" />
<el-table-column prop="phone" label="手机号" />
<el-table-column prop="email" label="邮箱" />
</el-table>
<el-pagination
background
layout="prev, pager, next"
:total="1000"
:page-size="20"
:current-page="1"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const tableData = ref([
{
name: '张三',
phone: '12345678901',
email: '12345678901@qq.com',
},
])
const search = ref('')
</script>
<style scoped>
.user-search {
margin-bottom: 10px;
}
</style>

1
src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

31
tsconfig.json Normal file
View File

@ -0,0 +1,31 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": "./", //
"paths": {
//baseUrl
"@/*": ["src/*"]
},
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

10
tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

90
vite.config.ts Normal file
View File

@ -0,0 +1,90 @@
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { viteMockServe } from 'vite-plugin-mock'
// 引入 fs 模块
// import fs from 'fs'
// const fs = require('fs');
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
return {
plugins: [
vue(),
AutoImport({
resolvers: [
ElementPlusResolver(),
IconsResolver({
prefix: 'Icon',
}),
],
}),
Components({
resolvers: [
ElementPlusResolver({importStyle:"sass"}),
IconsResolver({
enabledCollections: ['ep'],
}),
],
}),
Icons({
autoInstall: true,
}),
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/default'), path.resolve(process.cwd(), 'src/assets/icons/bosch')],
// Specify symbolId format
symbolId: 'icon-[dir]-[name]',
}),
viteMockServe({
localEnabled: command === 'serve',
}),
],
resolve: {
alias: {
'@': path.resolve('./src'), // 相对路径别名配置,使用 @ 代替 src
},
},
css: { //配置全局scss
preprocessorOptions: {
scss: {
quietDeps: true,
logger: {
warn: () => {}
},
javascriptEnabled: true,
// additionalData: `@use "@/styles/element/index.scss" as *;`,
},
},
},
// 代理跨域
server: {
// https: {
// key: fs.readFileSync('./cert.key'), // 证书密钥路径
// cert: fs.readFileSync('./cert.crt') // 证书文件路径
// },
host: '0.0.0.0',
proxy: {
[env.VITE_APP_BASE_API]: {
target: env.VITE_SERVE,
changeOrigin: true,
rewrite: (path) =>
path.replace(new RegExp(`^${env.VITE_APP_BASE_API}`), ''),
},
},
},
}
})