This commit is contained in:
2026-02-02 19:54:16 +08:00
commit 1b8a319258
42 changed files with 6174 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Environment variables
.env
.env.local
.env.*.local

0
README.md Normal file
View File

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue 3 PC Web</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1569
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "vue3-pc-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",
"less": "^4.2.0",
"vite": "^6.0.5"
}
}

391
src/App.vue Normal file
View File

@@ -0,0 +1,391 @@
<template>
<div class="app">
<div class="viewport-wrapper" :style="viewportStyle">
<header class="header">
<div class="left">
<div class="logo-wrap"></div>
<div class="route-list">
<div class="route-item">IP版权库</div>
<div class="route-item">推广任务</div>
<div class="route-item">AI创作工具</div>
<div class="route-item">资产库</div>
<div class="route-item">我的作品</div>
</div>
</div>
<div class="right">
<div class="login-list">
<div class="login-item">订阅</div>
<div class="login-item">登录</div>
</div>
</div>
</header>
<div class="content-wrapper">
<aside style="display: none;" class="sidebar">
<div class="choose-image-or-video-wrap">
<div class="section-btn section-image">
<span></span>
</div>
<div class="section-btn section-video choosed">
<span></span>
</div>
</div>
<router-link to="/dashboard" active-class="active-sidebar-item">
<div class="sidebar-item">
<div class="sidebar-item__icon">
<span>ICON</span>
</div>
<div class="sidebar-item__text">
<span>图生视频</span>
</div>
</div>
</router-link>
<router-link to="/product" active-class="active-sidebar-item">
<div class="sidebar-item">
<div class="sidebar-item__icon">
<span>ICON</span>
</div>
<div class="sidebar-item__text">
<span>参考生视频</span>
</div>
</div>
</router-link>
<!-- <nav class="sidebar-nav">
<ul>
<li class="nav-item">
<router-link to="/dashboard" active-class="active">
<span class="nav-icon">📊</span>
<span class="nav-text">数据统计</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/product" active-class="active">
<span class="nav-icon">📦</span>
<span class="nav-text">产品管理</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/user" active-class="active">
<span class="nav-icon">👥</span>
<span class="nav-text">用户管理</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/notification" active-class="active">
<span class="nav-icon">🔔</span>
<span class="nav-text">通知中心</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/setting" active-class="active">
<span class="nav-icon"></span>
<span class="nav-text">系统设置</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/document" active-class="active">
<span class="nav-icon">📚</span>
<span class="nav-text">文档中心</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/message" active-class="active">
<span class="nav-icon">💬</span>
<span class="nav-text">消息管理</span>
</router-link>
</li>
<li class="nav-item">
<router-link to="/analytics" active-class="active">
<span class="nav-icon">📈</span>
<span class="nav-text">数据分析</span>
</router-link>
</li>
</ul>
</nav> -->
</aside>
<main class="main">
<router-view />
</main>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
viewportStyle: this.resize()
}
},
methods: {
resize() {
console.log('wr-test-window.innerWidth', window.innerWidth);
const designWidth = 1920;
const designHeight = 1080;
if (window.innerWidth >= 1920) {
const result = {
transform: `scale(1)`,
width: `100%`,
height: `100%`,
transformOrigin: 'left top'
};
this.viewportStyle = result;
return result;
}
const scale = Math.min(window.innerWidth / designWidth, 1);
const result = {
transform: `scale(${scale})`,
width: `${designWidth}px`,
height: `${designHeight}px`,
transformOrigin: 'left top'
};
this.viewportStyle = result;
return result;
}
},
mounted() {
this.resize();
window.addEventListener('resize', this.resize);
},
beforeDestroy() {
window.removeEventListener('resize', this.resize);
}
}
</script>
<style lang="less" scoped>
.app {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: @body-bg-color;
background-image: url('./assets/imgs/background/index-bg.png');
.img-bg();
overflow: auto;
position: relative;
}
.viewport-wrapper {
position: absolute;
left: 0;
top: 0;
}
.header {
width: 100%;
height: @header-height;
backdrop-filter: blur(30px);
background: rgba(162, 162, 162, 0.2);
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 70px;
.left {
.display-row-left()
}
.right {
.display-row-right()
}
.logo-wrap {
width: 150px;
height: 35px;
background-image: url('./assets/imgs/logo/logo.png');
.img-bg();
margin-right: 40px;
}
.route-list {
display: flex;
justify-content: flex-start;
align-items: center;
.route-item {
width: 154px;
height: 54px;
font-weight: bold;
font-size: 20px;
color: @text-color;
.display-row-center();
cursor: pointer;
}
}
.login-list {
display: flex;
justify-content: flex-end;
align-content: center;
.login-item {
width: 100px;
height: 54px;
font-weight: bold;
font-size: 20px;
color: #FFFFFF;
.display-row-center();
}
}
}
.content-wrapper {
display: flex;
flex: 1;
overflow: hidden;
justify-content: flex-start;
align-items: flex-start;
min-height: @main-height;
}
.sidebar {
width: 120px;
background-color: @block-bg;
height: calc(100vh - 73px - 4px); /* 减去header和margin */
overflow-y: auto;
flex-shrink: 0;
margin-right: 4px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
padding: 16px 0;
.choose-image-or-video-wrap {
width: 80px;
height: 31px;
background: @body-bg-color;
border-radius: 2px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 4px;
margin-bottom: 15px;
.section-btn {
width: 34px;
height: 21px;
text-align: center;
background-color: @not-checked-section-bg;
color: @not-checked-section-color;
}
.choosed {
background-color: @checked-section-bg;
span {
.highlight-text()
}
}
}
.sidebar-item {
margin-bottom: 15px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
padding: 15px 0;
width: 80px;
height: 80px;
background-color: @not-checked-section-bg;
.sidebar-item__icon {
width: 30px;
height: 30px;
span {
color: @text-color;
}
}
.sidebar-item__text {
font-size: 12px;
span {
color: @text-color;
}
}
}
.active-sidebar-item {
.sidebar-item {
background-color: @body-bg-color;
.sidebar-item__icon {
span {
.highlight-text()
}
}
.sidebar-item__text {
span {
.highlight-text()
}
}
}
}
.sidebar-nav {
padding: 16px 0;
ul {
list-style: none;
margin: 0;
padding: 0;
}
.nav-item {
margin-bottom: 4px;
&.active {
a {
background-color: #ecf5ff;
color: #409eff;
border-right: 3px solid #409eff;
}
}
a {
display: flex;
align-items: center;
padding: 12px 24px;
color: #303133;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
border-right: 3px solid transparent;
&:hover {
background-color: #f5f7fa;
color: #409eff;
}
}
.nav-icon {
font-size: 16px;
margin-right: 12px;
width: 20px;
text-align: center;
}
.nav-text {
flex: 1;
}
}
}
}
.main {
flex: 1;
background-color: transparent;
overflow-y: auto;
width: 100%;
// height: 100%;
min-height: @main-height;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 965 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1019 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 883 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,180 @@
<template>
<!-- RadioGroup 组件容器 -->
<div class="custom-radio-group" :class="{ 'is-disabled': disabled }">
<!-- 可选的标题 -->
<div v-if="label" class="radio-group-label">{{ label }}</div>
<!-- Radio 选项列表 -->
<div class="radio-options">
<div
v-for="option in options"
:key="option.value"
class="radio-option"
:class="{ 'is-checked': option.value === modelValue, 'is-disabled': disabled }"
@click="selectOption(option)"
>
<!-- Radio 按钮 -->
<div class="radio-button">
<div class="radio-inner" :class="{ 'is-checked': option.value === modelValue }"></div>
</div>
<!-- Radio 标签 -->
<div class="radio-label">{{ option.label }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CustomRadioGroup',
props: {
// 绑定值,支持 v-model
modelValue: {
type: [String, Number, Boolean],
default: ''
},
// 选项列表
options: {
type: Array,
default: () => []
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 组标签
label: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'change'],
methods: {
// 选择选项
selectOption(option) {
if (this.disabled) return;
// 更新 v-model 绑定值
this.$emit('update:modelValue', option.value);
// 触发 change 事件
this.$emit('change', option.value, option);
}
}
}
</script>
<style lang="less" scoped>
/* RadioGroup 组件容器 */
.custom-radio-group {
width: 100%;
/* 禁用状态 */
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 组标签 */
.radio-group-label {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 12px;
}
/* Radio 选项列表 - 一行内布局 */
.radio-options {
display: flex;
width: 100%;
gap: 8px;
}
/* Radio 选项 - flex布局且宽度等分 */
.radio-option {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 12px 0;
background-color: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
user-select: none;
/* 选中状态 */
&.is-checked {
background-color: #ecf5ff;
border-color: #409eff;
color: #409eff;
}
/* 悬停状态 */
&:not(.is-disabled):hover {
background-color: #ecf5ff;
border-color: #c6e2ff;
}
/* 禁用状态 */
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
}
/* Radio 按钮 */
.radio-button {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-right: 8px;
/* Radio 内部圆点 */
.radio-inner {
width: 12px;
height: 12px;
border: 2px solid #dcdfe6;
border-radius: 50%;
background-color: #fff;
transition: all 0.3s;
position: relative;
/* 选中状态的内部圆点 */
&.is-checked {
border-color: #409eff;
background-color: #409eff;
/* 选中后的小圆点 */
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4px;
height: 4px;
background-color: #fff;
border-radius: 50%;
}
}
}
}
/* Radio 标签 */
.radio-label {
font-size: 14px;
color: #303133;
font-weight: 500;
/* 选中状态的标签颜色 */
.radio-option.is-checked & {
color: #409eff;
}
}
}
</style>

View File

@@ -0,0 +1,330 @@
<template>
<!-- Select 组件容器 -->
<div class="custom-select" :class="{ 'is-open': isOpen, 'is-disabled': disabled }">
<!-- 选择框主体 -->
<div
class="select-trigger"
@click.stop="toggleDropdown"
>
<!-- 选中的值 -->
<span class="select-value">{{ selectedLabel || placeholder }}</span>
<!-- 下拉箭头 -->
<span class="select-arrow"></span>
</div>
<!-- 下拉选项列表 -->
<div v-if="isOpen" class="select-dropdown">
<!-- 搜索框可选 -->
<div v-if="filterable" class="select-search">
<input
type="text"
class="search-input"
placeholder="搜索选项..."
v-model="searchValue"
@input="handleSearch"
/>
</div>
<!-- 选项列表 -->
<div class="select-options">
<!-- 加载状态 -->
<div v-if="loading" class="select-loading">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<!-- 无选项状态 -->
<div v-else-if="filteredOptions.length === 0" class="select-empty">
无匹配选项
</div>
<!-- 选项列表 -->
<div
v-for="option in filteredOptions"
:key="option.value"
class="select-option"
:class="{ 'is-selected': option.value === modelValue }"
@click="selectOption(option)"
>
{{ option.label }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CustomSelect',
props: {
// 绑定值,支持 v-model
modelValue: {
type: [String, Number, Boolean],
default: ''
},
// 选项列表
options: {
type: Array,
default: () => []
},
// 占位符
placeholder: {
type: String,
default: '请选择'
},
// 是否可搜索
filterable: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 是否加载中
loading: {
type: Boolean,
default: false
}
},
emits: ['update:modelValue', 'change', 'search'],
data() {
return {
isOpen: true, // 下拉框是否打开
searchValue: '', // 搜索值
selectedLabel: '' // 选中的标签
}
},
computed: {
// 过滤后的选项
filteredOptions() {
if (!this.searchValue) {
return this.options
}
return this.options.filter(option =>
option.label.toLowerCase().includes(this.searchValue.toLowerCase())
)
}
},
watch: {
// 监听绑定值变化,更新选中标签
modelValue: {
handler(newValue) {
this.updateSelectedLabel(newValue)
},
immediate: true
},
// 监听选项列表变化,更新选中标签
options: {
handler() {
this.updateSelectedLabel(this.modelValue)
},
deep: true
}
},
mounted() {
// 点击外部关闭下拉框
document.addEventListener('click', this.closeDropdown)
},
beforeUnmount() {
// 移除事件监听
document.removeEventListener('click', this.closeDropdown)
},
methods: {
// 更新选中标签
updateSelectedLabel(value) {
const option = this.options.find(opt => opt.value === value)
this.selectedLabel = option ? option.label : ''
},
// 切换下拉框显示/隐藏
toggleDropdown() {
if (this.disabled) return
this.isOpen = !this.isOpen;
console.log('wr-test-isOpen', this.isOpen);
},
// 关闭下拉框
closeDropdown(e) {
if (!this.$el.contains(e.target)) {
this.isOpen = false
}
},
// 选择选项
selectOption(option) {
// 更新 v-model 绑定值
this.$emit('update:modelValue', option.value)
// 触发 change 事件
this.$emit('change', option.value, option)
// 关闭下拉框
this.isOpen = false
// 清空搜索值
this.searchValue = ''
},
// 处理搜索
handleSearch() {
this.$emit('search', this.searchValue)
}
}
}
</script>
<style lang="less" scoped>
/* Select 组件容器 */
.custom-select {
position: relative;
width: 100%;
/* 打开状态 */
&.is-open {
.select-arrow {
transform: rotate(180deg);
}
}
/* 禁用状态 */
&.is-disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 选择框主体 */
.select-trigger {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
background-color: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
color: #303133;
&:hover {
border-color: #c6e2ff;
}
&:focus {
outline: none;
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
}
/* 选中的值 */
.select-value {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* 占位符样式 */
.custom-select:not(.is-open) & {
color: #909399;
}
}
/* 下拉箭头 */
.select-arrow {
font-size: 12px;
color: #909399;
transition: transform 0.3s;
margin-left: 10px;
}
/* 下拉选项列表 */
.select-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 4px;
background-color: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 10;
}
/* 搜索框 */
.select-search {
padding: 10px;
border-bottom: 1px solid #f5f7fa;
}
.search-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
outline: none;
&:focus {
border-color: #409eff;
}
}
/* 选项列表 */
.select-options {
max-height: 200px;
overflow-y: auto;
}
/* 选项项 */
.select-option {
padding: 10px 14px;
cursor: pointer;
font-size: 14px;
color: #303133;
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
/* 选中状态 */
&.is-selected {
background-color: #ecf5ff;
color: #409eff;
}
}
/* 加载状态 */
.select-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
color: #909399;
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 8px;
}
}
/* 无选项状态 */
.select-empty {
padding: 20px;
text-align: center;
color: #909399;
}
}
/* 加载动画 */
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>

View File

@@ -0,0 +1,108 @@
<template>
<!-- Switch 组件容器 -->
<div
class="custom-switch"
:class="{ 'is-checked': modelValue, 'is-disabled': disabled }"
@click="toggle"
>
<!-- 开关轨道 -->
<div class="switch-track">
<!-- 开关滑块 -->
<div class="switch-thumb"></div>
</div>
<!-- 可选的标签文字 -->
<span v-if="label" class="switch-label">{{ label }}</span>
</div>
</template>
<script>
export default {
name: 'CustomSwitch',
props: {
// 绑定值,支持 v-model
modelValue: {
type: Boolean,
default: false
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 开关标签
label: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'change'],
methods: {
// 切换开关状态
toggle() {
if (this.disabled) return;
const newValue = !this.modelValue;
// 更新 v-model 绑定值
this.$emit('update:modelValue', newValue);
// 触发 change 事件
this.$emit('change', newValue);
}
}
}
</script>
<style lang="less" scoped>
/* 开关容器 */
.custom-switch {
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
/* 禁用状态 */
&.is-disabled {
cursor: not-allowed;
opacity: 0.6;
}
/* 选中状态的轨道颜色 */
&.is-checked .switch-track {
background-color: #409eff;
}
/* 选中状态的滑块位置 */
&.is-checked .switch-thumb {
transform: translateX(20px);
}
}
/* 开关轨道 */
.switch-track {
position: relative;
width: 44px;
height: 24px;
background-color: #dcdfe6;
border-radius: 12px;
transition: background-color 0.3s;
}
/* 开关滑块 */
.switch-thumb {
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
background-color: #fff;
border-radius: 50%;
transition: transform 0.3s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 开关标签 */
.switch-label {
margin-left: 8px;
font-size: 14px;
color: #303133;
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<!-- Textarea 组件容器 -->
<div class="custom-textarea-wrapper">
<!-- Textarea 主体 -->
<div class="textarea-container">
<textarea
ref="textareaRef"
class="custom-textarea"
:value="modelValue"
@input="handleInput"
@change="handleChange"
@blur="handleBlur"
@focus="handleFocus"
:placeholder="placeholder"
:disabled="disabled"
:rows="rows"
:cols="cols"
:maxlength="maxlength"
></textarea>
<!-- 左下角 slot -->
<div class="textarea-slot textarea-slot-left">
<slot name="left"></slot>
</div>
<!-- 右下角 slot -->
<div class="textarea-slot textarea-slot-right">
<slot name="right"></slot>
<!-- 可选的字数统计 -->
<span v-if="showWordLimit" class="word-limit">
{{ currentLength }}/{{ maxlength }}
</span>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CustomTextarea',
props: {
// 绑定值,支持 v-model
modelValue: {
type: String,
default: ''
},
// 占位符
placeholder: {
type: String,
default: '请输入内容...'
},
// 行数
rows: {
type: Number,
default: 4
},
// 列数
cols: {
type: Number,
default: 50
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 最大长度
maxlength: {
type: Number,
default: 0
},
// 是否显示字数统计
showWordLimit: {
type: Boolean,
default: false
}
},
emits: ['update:modelValue', 'input', 'change', 'blur', 'focus'],
data() {
return {
currentLength: 0 // 当前输入长度
}
},
watch: {
// 监听绑定值变化,更新当前长度
modelValue: {
handler(newValue) {
this.currentLength = newValue.length
},
immediate: true
}
},
methods: {
// 处理输入事件
handleInput(e) {
const value = e.target.value
// 更新当前长度
this.currentLength = value.length
// 更新 v-model 绑定值
this.$emit('update:modelValue', value)
// 触发 input 事件
this.$emit('input', e)
},
// 处理 change 事件
handleChange(e) {
this.$emit('change', e)
},
// 处理 blur 事件
handleBlur(e) {
this.$emit('blur', e)
},
// 处理 focus 事件
handleFocus(e) {
this.$emit('focus', e)
}
}
}
</script>
<style lang="less" scoped>
/* Textarea 组件容器 */
.custom-textarea-wrapper {
width: 100%;
}
/* Textarea 容器(用于定位 slot */
.textarea-container {
position: relative;
width: 100%;
}
/* Textarea 主体样式 */
.custom-textarea {
width: 100%;
min-height: 100px;
padding: 12px;
background-color: #f5f7fa; /* 自定义背景色 */
color: #303133; /* 自定义文字色 */
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
line-height: 1.5;
outline: none;
transition: all 0.3s;
resize: vertical; /* 允许垂直调整大小 */
/* 占位符样式 */
&::placeholder {
color: #909399; /* 自定义 placeholder 颜色 */
}
/* 聚焦状态 */
&:focus {
border-color: #409eff;
background-color: #fff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
/* 禁用状态 */
&:disabled {
background-color: #f5f7fa;
color: #c0c4cc;
cursor: not-allowed;
opacity: 0.6;
}
/* 自定义滚动条样式 */
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
&:hover {
background: #a8a8a8;
}
}
}
/* Slot 容器 */
.textarea-slot {
position: absolute;
font-size: 12px;
color: #909399;
padding: 4px 8px;
pointer-events: none;
}
/* 左下角 slot */
.textarea-slot-left {
bottom: 4px;
left: 4px;
}
/* 右下角 slot */
.textarea-slot-right {
bottom: 4px;
right: 4px;
display: flex;
align-items: center;
gap: 8px;
}
/* 字数统计 */
.word-limit {
font-size: 12px;
color: #909399;
}
</style>

8
src/main.js Normal file
View File

@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './styles/theme.less'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')

108
src/router/index.js Normal file
View File

@@ -0,0 +1,108 @@
import { createRouter, createWebHistory } from 'vue-router'
// 导入页面组件
const Dashboard = () => import('../views/Dashboard.vue')
const Index = () => import('../views/Index.vue')
const Product = () => import('../views/Product.vue')
const User = () => import('../views/User.vue')
const Notification = () => import('../views/Notification.vue')
const Setting = () => import('../views/Setting.vue')
const Document = () => import('../views/Document.vue')
const Message = () => import('../views/Message.vue')
const Analytics = () => import('../views/Analytics.vue')
const routes = [
{
path: '/',
redirect: '/index'
},
{
path: '/index',
name: 'Index',
component: Index,
meta: {
title: '首页',
icon: '📊'
}
},
{
path: '/product',
name: 'Product',
component: Product,
meta: {
title: '产品管理',
icon: '📦'
}
},
{
path: '/user',
name: 'User',
component: User,
meta: {
title: '用户管理',
icon: '👥'
}
},
{
path: '/notification',
name: 'Notification',
component: Notification,
meta: {
title: '通知中心',
icon: '🔔'
}
},
{
path: '/setting',
name: 'Setting',
component: Setting,
meta: {
title: '系统设置',
icon: '⚙️'
}
},
{
path: '/document',
name: 'Document',
component: Document,
meta: {
title: '文档中心',
icon: '📚'
}
},
{
path: '/message',
name: 'Message',
component: Message,
meta: {
title: '消息管理',
icon: '💬'
}
},
{
path: '/analytics',
name: 'Analytics',
component: Analytics,
meta: {
title: '数据分析',
icon: '📈'
}
}
]
const router = createRouter({
history: createWebHistory(), // 使用history模式而非hash模式
routes
})
// 路由守卫,设置页面标题
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = `${to.meta.title} - Vue 3 PC Web`
}
next()
})
export default router

115
src/styles/theme.less Normal file
View File

@@ -0,0 +1,115 @@
// ---------全局变量------------ 开始
@body-bg-color: #000000;
@text-color: #ffffff;
@button-bg: linear-gradient( 230deg, #FBC7FF 0%, #A8FFFF 34.48%, #FFFBDB 68.47%, #E9E0FF 100%);
@block-bg: #24222D;
@checked-section-bg: #313131 !important;
@checked-section-color: @button-bg !important;
@not-checked-section-bg: transparent;
@not-checked-section-color: #717171;
@header-height: 93px;
@main-height: calc(1080px - @header-height);
// ---------全局变量------------ 结束
// ---------基础样式------------ 开始
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: @body-bg-color;
color: @text-color;
font-family: Source Han Sans CN, Source Han Sans CN;
font-size: 14px;
line-height: 1.5;
}
::-webkit-scrollbar {
width: 4px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #505050;
}
::-webkit-scrollbar-thumb {
background: #ffffff;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: fade(@text-color, 40%);
}
textarea {
width: 100%;
height: 100%;
background-color: transparent;
color: #ffffff;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 20px;
line-height: 28px; // 新增行高设置
border: none;
resize: none;
&:hover {
border-color: none;
}
&:focus {
border-color: none;
outline: none; /* 移除浏览器默认的 outline */
}
&::placeholder { /* Standard syntax */
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 20px;
color: #767676;
line-height: 28px; // 新增行高设置
}
}
// ---------基础样式------------ 结束
// ---------常用类定义------------ 开始
.highlight-text {
background-image: @button-bg;
color: transparent;
-webkit-background-clip: text;
background-clip: text;
}
.display-row-center {
display: flex;
justify-content: center;
align-items: center;
}
.center {
.display-row-center();
}
.display-row-left {
display: flex;
justify-content: flex-start;
align-items: center;
}
.display-row-right {
display: flex;
justify-content: flex-end;
align-items: center;
}
.img-bg {
background-repeat: no-repeat;
background-size: 100%;
background-position: center center;
}
// ---------常用类定义------------ 结束

438
src/views/Analytics.vue Normal file
View File

@@ -0,0 +1,438 @@
<template>
<div class="page-container">
<h2 class="page-title">📈 数据分析</h2>
<div class="page-content">
<div class="time-range-selector">
<button class="range-btn active">今日</button>
<button class="range-btn">昨日</button>
<button class="range-btn">本周</button>
<button class="range-btn">本月</button>
<button class="range-btn">自定义</button>
</div>
<div class="analytics-grid">
<div class="card">
<h3>流量分析</h3>
<div class="chart-placeholder line-chart">
<div class="line-pattern"></div>
</div>
</div>
<div class="card">
<h3>转化率分析</h3>
<div class="conversion-steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-info">
<div class="step-title">访问</div>
<div class="step-value">10,000</div>
</div>
<div class="step-arrow"></div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-info">
<div class="step-title">浏览</div>
<div class="step-value">7,500</div>
<div class="step-rate">75%</div>
</div>
<div class="step-arrow"></div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-info">
<div class="step-title">注册</div>
<div class="step-value">1,500</div>
<div class="step-rate">20%</div>
</div>
<div class="step-arrow"></div>
</div>
<div class="step">
<div class="step-number">4</div>
<div class="step-info">
<div class="step-title">购买</div>
<div class="step-value">300</div>
<div class="step-rate">20%</div>
</div>
</div>
</div>
</div>
<div class="card">
<h3>用户来源</h3>
<div class="source-list">
<div class="source-item">
<div class="source-name">直接访问</div>
<div class="source-bar">
<div class="source-progress" style="width: 45%;"></div>
</div>
<div class="source-percent">45%</div>
</div>
<div class="source-item">
<div class="source-name">搜索引擎</div>
<div class="source-bar">
<div class="source-progress" style="width: 30%;"></div>
</div>
<div class="source-percent">30%</div>
</div>
<div class="source-item">
<div class="source-name">社交媒体</div>
<div class="source-bar">
<div class="source-progress" style="width: 15%;"></div>
</div>
<div class="source-percent">15%</div>
</div>
<div class="source-item">
<div class="source-name">其他</div>
<div class="source-bar">
<div class="source-progress" style="width: 10%;"></div>
</div>
<div class="source-percent">10%</div>
</div>
</div>
</div>
<div class="card">
<h3>热门页面</h3>
<div class="top-pages">
<div class="page-item">
<div class="page-rank">1</div>
<div class="page-info">
<div class="page-title">首页</div>
<div class="page-path">/</div>
</div>
<div class="page-views">5,678</div>
</div>
<div class="page-item">
<div class="page-rank">2</div>
<div class="page-info">
<div class="page-title">产品列表</div>
<div class="page-path">/products</div>
</div>
<div class="page-views">3,456</div>
</div>
<div class="page-item">
<div class="page-rank">3</div>
<div class="page-info">
<div class="page-title">详情页</div>
<div class="page-path">/product/123</div>
</div>
<div class="page-views">2,345</div>
</div>
<div class="page-item">
<div class="page-rank">4</div>
<div class="page-info">
<div class="page-title">关于我们</div>
<div class="page-path">/about</div>
</div>
<div class="page-views">1,234</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Analytics'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.time-range-selector {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.range-btn {
padding: 8px 16px;
border: 1px solid #dcdfe6;
background-color: #fff;
color: #606266;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s;
&:hover {
color: #409eff;
border-color: #c6e2ff;
}
&.active {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
}
.analytics-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.card {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.card h3 {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
.chart-placeholder {
height: 200px;
background-color: #f5f7fa;
border-radius: 4px;
position: relative;
overflow: hidden;
}
.line-chart {
display: flex;
align-items: center;
justify-content: center;
}
.line-pattern {
width: 100%;
height: 2px;
background-color: #dcdfe6;
position: relative;
&:before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
#409eff 10px,
#409eff 20px
);
opacity: 0.3;
}
&:after {
content: '';
position: absolute;
top: -1px;
left: 20%;
right: 30%;
height: 4px;
background-color: #409eff;
border-radius: 2px;
box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
}
}
.conversion-steps {
display: flex;
flex-direction: column;
gap: 20px;
}
.step {
display: flex;
align-items: center;
position: relative;
&:not(:last-child) {
&:after {
content: '';
position: absolute;
left: 25px;
top: 50px;
width: 2px;
height: 20px;
background-color: #dcdfe6;
}
}
}
.step-number {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #409eff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 600;
margin-right: 20px;
flex-shrink: 0;
}
.step-info {
flex: 1;
}
.step-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.step-value {
font-size: 24px;
font-weight: 700;
color: #409eff;
margin-bottom: 4px;
}
.step-rate {
font-size: 14px;
color: #67c23a;
font-weight: 500;
}
.step-arrow {
font-size: 20px;
color: #dcdfe6;
margin-left: 20px;
font-weight: bold;
}
.source-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.source-item {
display: flex;
align-items: center;
gap: 16px;
}
.source-name {
width: 100px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
.source-bar {
flex: 1;
height: 8px;
background-color: #f0f2f5;
border-radius: 4px;
overflow: hidden;
}
.source-progress {
height: 100%;
background-color: #409eff;
border-radius: 4px;
transition: width 0.3s;
}
.source-percent {
width: 50px;
font-size: 14px;
font-weight: 600;
color: #303133;
text-align: right;
}
.top-pages {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid #f5f7fa;
&:last-child {
border-bottom: none;
}
}
.page-rank {
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #f5f7fa;
color: #606266;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
&:nth-child(1) {
background-color: #fde2e2;
color: #f56c6c;
}
&:nth-child(2) {
background-color: #e1f3d8;
color: #67c23a;
}
&:nth-child(3) {
background-color: #ebe5fe;
color: #909399;
}
}
.page-info {
flex: 1;
}
.page-title {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.page-path {
font-size: 12px;
color: #909399;
}
.page-views {
font-size: 16px;
font-weight: 600;
color: #409eff;
}
</style>

200
src/views/Dashboard.vue Normal file
View File

@@ -0,0 +1,200 @@
<template>
<div class="page-container">
<h2 class="page-title">📊 数据统计</h2>
<div class="page-content">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number">12,345</div>
<div class="stat-label">总访问量</div>
<div class="stat-change positive">+12.5%</div>
</div>
<div class="stat-card">
<div class="stat-number">6,789</div>
<div class="stat-label">活跃用户</div>
<div class="stat-change positive">+8.3%</div>
</div>
<div class="stat-card">
<div class="stat-number">¥98,765</div>
<div class="stat-label">总收入</div>
<div class="stat-change positive">+15.2%</div>
</div>
<div class="stat-card">
<div class="stat-number">456</div>
<div class="stat-label">订单数</div>
<div class="stat-change negative">-2.1%</div>
</div>
</div>
<div class="charts-section">
<div class="card">
<h3>访问趋势</h3>
<div class="chart-placeholder">
<div class="chart-bar" style="height: 60%;"></div>
<div class="chart-bar" style="height: 85%;"></div>
<div class="chart-bar" style="height: 70%;"></div>
<div class="chart-bar" style="height: 95%;"></div>
<div class="chart-bar" style="height: 80%;"></div>
<div class="chart-bar" style="height: 90%;"></div>
<div class="chart-bar" style="height: 75%;"></div>
</div>
</div>
<div class="card">
<h3>用户分布</h3>
<div class="pie-chart-placeholder">
<div class="pie-slice slice-1"></div>
<div class="pie-slice slice-2"></div>
<div class="pie-slice slice-3"></div>
<div class="pie-slice slice-4"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Dashboard'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
}
.stat-number {
font-size: 32px;
font-weight: 700;
color: #303133;
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: #606266;
margin-bottom: 12px;
}
.stat-change {
font-size: 12px;
font-weight: 500;
&.positive {
color: #67c23a;
}
&.negative {
color: #f56c6c;
}
}
.charts-section {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.card {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.card h3 {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 20px;
}
.chart-placeholder {
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 200px;
background-color: #f5f7fa;
border-radius: 4px;
padding: 20px;
}
.chart-bar {
width: 30px;
background-color: #409eff;
border-radius: 4px 4px 0 0;
transition: all 0.3s;
&:hover {
background-color: #66b1ff;
transform: scaleY(1.05);
}
}
.pie-chart-placeholder {
position: relative;
width: 100%;
height: 200px;
background-color: #f5f7fa;
border-radius: 50%;
overflow: hidden;
}
.pie-slice {
position: absolute;
width: 100%;
height: 100%;
&.slice-1 {
background-color: #409eff;
clip-path: polygon(50% 50%, 50% 0%, 100% 0%, 100% 50%);
}
&.slice-2 {
background-color: #67c23a;
clip-path: polygon(50% 50%, 100% 50%, 100% 100%, 50% 100%);
}
&.slice-3 {
background-color: #e6a23c;
clip-path: polygon(50% 50%, 50% 100%, 0% 100%, 0% 50%);
}
&.slice-4 {
background-color: #f56c6c;
clip-path: polygon(50% 50%, 0% 50%, 0% 0%, 50% 0%);
}
}
</style>

217
src/views/Document.vue Normal file
View File

@@ -0,0 +1,217 @@
<template>
<div class="page-container">
<h2 class="page-title">📚 文档中心</h2>
<div class="page-content">
<div class="documents-grid">
<div class="document-card">
<div class="document-icon">📄</div>
<div class="document-info">
<div class="document-title">API 文档</div>
<div class="document-desc">系统 API 接口详细说明</div>
<div class="document-meta">
<span class="document-date">2024-01-20</span>
<span class="document-size">1.2 MB</span>
</div>
</div>
<div class="document-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small">下载</button>
</div>
</div>
<div class="document-card">
<div class="document-icon">📋</div>
<div class="document-info">
<div class="document-title">使用手册</div>
<div class="document-desc">系统功能使用指南</div>
<div class="document-meta">
<span class="document-date">2024-01-15</span>
<span class="document-size">856 KB</span>
</div>
</div>
<div class="document-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small">下载</button>
</div>
</div>
<div class="document-card">
<div class="document-icon">📊</div>
<div class="document-info">
<div class="document-title">数据字典</div>
<div class="document-desc">系统数据结构说明</div>
<div class="document-meta">
<span class="document-date">2024-01-10</span>
<span class="document-size">523 KB</span>
</div>
</div>
<div class="document-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small">下载</button>
</div>
</div>
<div class="document-card">
<div class="document-icon">🔧</div>
<div class="document-info">
<div class="document-title">部署指南</div>
<div class="document-desc">系统部署和配置说明</div>
<div class="document-meta">
<span class="document-date">2024-01-05</span>
<span class="document-size">345 KB</span>
</div>
</div>
<div class="document-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small">下载</button>
</div>
</div>
</div>
<div class="document-categories">
<h3>文档分类</h3>
<div class="categories-list">
<span class="category-tag active">全部</span>
<span class="category-tag">API 文档</span>
<span class="category-tag">使用手册</span>
<span class="category-tag">开发文档</span>
<span class="category-tag">部署文档</span>
<span class="category-tag">其他</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Document'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.documents-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.document-card {
display: flex;
align-items: flex-start;
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
}
.document-icon {
font-size: 24px;
margin-right: 16px;
margin-top: 4px;
}
.document-info {
flex: 1;
}
.document-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.document-desc {
font-size: 14px;
color: #606266;
margin-bottom: 12px;
line-height: 1.5;
}
.document-meta {
display: flex;
align-items: center;
gap: 12px;
.document-date,
.document-size {
font-size: 12px;
color: #909399;
}
}
.document-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.document-categories {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.document-categories h3 {
font-size: 18px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
}
.categories-list {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.category-tag {
display: inline-block;
padding: 8px 16px;
background-color: #f5f7fa;
color: #606266;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
&:hover {
background-color: #ecf5ff;
color: #409eff;
}
&.active {
background-color: #409eff;
color: #fff;
}
}
</style>

330
src/views/Index.vue Normal file
View File

@@ -0,0 +1,330 @@
<template>
<div class="page-container">
<div class="index-title">
AI 造万象创意不设限
</div>
<div class="model-section-group">
<div class="model-section section-raw checked">
<div class="section-title"></div>
<div class="section-desc">图片生成视频生成音频生成</div>
<div class="section__checked-icon"></div>
</div>
<div class="model-section section-episode">
<div class="section-title"></div>
<div class="section-desc">剧本生成分镜生成自动配乐</div>
<div class="section__checked-icon"></div>
</div>
</div>
<div class="generate">
<div class="generate__mode" :class="`generate__mode__choose-${generateMode}`">
<div class="generate__mode-item generate__mode-item__image" :class="generateMode === generate_TYPES.IMAGE ? 'choosed': ''">
<div class="generate__mode-item__inner">
<div class="generate__mode-item__icon generate__mode-item__image-icon"></div>
<div class="generate__mode-item__text">图片生成</div>
</div>
</div>
<div class="generate__mode-item generate__mode-item__video" :class="generateMode === generate_TYPES.VIDEO ? 'choosed': ''">
<div class="generate__mode-item__inner">
<div class="generate__mode-item__icon generate__mode-item__video-icon"></div>
<div class="generate__mode-item__text">视频生成</div>
</div>
</div>
</div>
<div class="generate__form">
<div class="generate__form-input">
<div class="generate__form-input__upload"></div>
<div class="generate__form-input__textarea">
<textarea placeholder="请输入你的创意按Enter发送"></textarea>
</div>
<div class="generate__form-input__right">
<div class="trans-switch">英文翻译</div>
</div>
</div>
<div class="generate__form-settings">
<model-select></model-select>
</div>
</div>
</div>
</div>
</template>
<script>
import modelSelect from './index/components/select/modelSelect.vue';
const generate_TYPES = {
IMAGE: 'image',
VIDEO: 'video'
}
export default {
name: 'Index',
components: { modelSelect },
data() {
return {
generate_TYPES,
generateMode: generate_TYPES.IMAGE
}
}
}
</script>
<style lang="less" scoped>
@indexImages: "../assets/imgs/index";
.page-container {
width: 100%;
min-height: @main-height;
background: transparent;
padding: 53px;
}
.index-title {
width: 100%;
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: bold;
font-size: 40px;
color: #FFFFFF;
margin: 0 auto 102px;
text-align: center;
}
.model-section-group {
width: 1200px;
display: flex;
justify-content: space-between;
align-content: center;
margin: 0 auto 45px;
.model-section {
width: 576px;
height: 94px;
border-radius: 12px;
background: transparent;
backdrop-filter: blur(30px);
background: rgba(162, 162, 162, 0.2);
padding-left: 36px;
padding-top: 24px;
position: relative;
.section {
&-title {
width: 120px;
height: 24px;
.img-bg();
margin-bottom: 8px;
}
&-desc {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: 400;
font-size: 14px;
color: #FAF2FF;
}
&__checked-icon {
position: absolute;
right: 0;
left: 0;
bottom: 0;
width: 576px;
height: 170px;
.img-bg();
display: none;
}
}
&.section {
&-episode {
.section {
&-title {
background-image: url('@{indexImages}/episode.png');
}
}
}
&-raw {
.section {
&-title {
background-image: url('@{indexImages}/raw.png');
}
}
}
}
&.checked {
&.model-section {
background: @button-bg;
}
& .section {
&-desc {
color: #000000 !important;
}
&__checked-icon {
display: block !important;
}
}
&.section {
&-raw {
.section {
&-title {
background-image: url('@{indexImages}/raw-checked.png');
}
&__checked-icon {
background-image: url('@{indexImages}/raw-checked-icon.png');
}
}
}
&-episode {
.section {
&-title {
background-image: url('@{indexImages}/episode-checked.png');
}
&__checked-icon {
background-image: url('@{indexImages}/episode-checked-icon.png');
}
}
}
}
}
}
}
.generate-galss {
backdrop-filter: blur(30px);
background: rgba(162, 162, 162, 0.2);
}
.generate-form-galss {
backdrop-filter: blur(30px);
background: rgba(162, 162, 162, 0.2);
}
.generate {
width: 1200px;
margin: 0 auto;
position: relative;
&__mode {
width: 402px;
height: 57px;
.display-row-left();
position: relative;
&-item {
width: 200px;
height: 57px;
border-radius: 20px 20px 0 0;
.generate-galss();
display: flex;
align-items: flex-start;
.img-bg();
&__inner {
width: 200px;
height: 57px;
.center();
}
&__image {
position: absolute;
left: 0;
top: 0;
height: 77px;
z-index: 2;
justify-content: flex-start;
}
&__video {
position: absolute;
right: 0;
top: 0;
width: 240px;
z-index: 1;
justify-content: flex-end;
}
&__icon {
width: 24px;
height: 24px;
.img-bg();
margin-right: 13px;
}
&__text {
font-weight: bold;
font-size: 20px;
color: #FFFFFF;
.center();
}
&__image-icon {
background-image: url('@{indexImages}/generate-image.png');
}
&__video-icon {
background-image: url('@{indexImages}/generate-video.png');
}
&.choosed {
&.generate__mode-item {
&__image {
background-image: url('@{indexImages}/image-choosed.png');
}
&__video {
background-image: url('@{indexImages}/video-choosed.png');
}
}
.generate__mode-item {
&__text {
color: #000000;
}
&__image-icon {
background-image: url('@{indexImages}/generate-image-choosed.png');
}
&__video-icon {
background-image: url('@{indexImages}/generate-video-choosed.png');
}
}
}
}
}
&__form {
position: absolute;
left: 0;
top: 56px;
z-index: 4;
width: 1200px;
min-height: 276px;
border-radius: 20px 20px 20px 20px;
border: 1px solid #DFBFF2;
.generate-form-galss();
padding: 24px;
&-input {
display: flex;
justify-content: space-between;
align-items: center;
height: 163px;
margin-bottom: 20px;
&__upload {
width: 126px;
height: 163px;
border-radius: 8px;
.generate-form-galss();
}
&__textarea {
width: 858px;
height: 163px;
}
&__right {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
}
}
}
}
</style>

407
src/views/Message.vue Normal file
View File

@@ -0,0 +1,407 @@
<template>
<div class="page-container">
<h2 class="page-title">💬 消息管理</h2>
<div class="page-content">
<div class="messages-layout">
<div class="messages-sidebar">
<div class="message-filters">
<button class="filter-btn active">全部消息</button>
<button class="filter-btn">未读消息</button>
<button class="filter-btn">已读消息</button>
<button class="filter-btn">发送消息</button>
</div>
<div class="message-threads">
<div class="thread-item active">
<div class="thread-avatar">A</div>
<div class="thread-info">
<div class="thread-name">张三</div>
<div class="thread-preview">你好请问有什么可以帮助你的吗</div>
</div>
<div class="thread-meta">
<div class="thread-time">10:30</div>
<div class="thread-badge">3</div>
</div>
</div>
<div class="thread-item">
<div class="thread-avatar">B</div>
<div class="thread-info">
<div class="thread-name">李四</div>
<div class="thread-preview">关于项目进度的更新</div>
</div>
<div class="thread-meta">
<div class="thread-time">昨天</div>
</div>
</div>
<div class="thread-item">
<div class="thread-avatar">C</div>
<div class="thread-info">
<div class="thread-name">王五</div>
<div class="thread-preview">谢谢收到了</div>
</div>
<div class="thread-meta">
<div class="thread-time">2天前</div>
</div>
</div>
</div>
</div>
<div class="messages-main">
<div class="message-header">
<div class="header-info">
<div class="header-name">张三</div>
<div class="header-status">在线</div>
</div>
<div class="header-actions">
<button class="btn btn-small">📞</button>
<button class="btn btn-small">📷</button>
<button class="btn btn-small"></button>
</div>
</div>
<div class="messages-content">
<div class="message received">
<div class="message-bubble">
<div class="message-text">你好请问有什么可以帮助你的吗</div>
<div class="message-time">10:25</div>
</div>
</div>
<div class="message sent">
<div class="message-bubble">
<div class="message-text">你好我想了解一下产品的详细信息</div>
<div class="message-time">10:26</div>
</div>
</div>
<div class="message received">
<div class="message-bubble">
<div class="message-text">当然可以请问你想了解哪方面的信息呢</div>
<div class="message-time">10:27</div>
</div>
</div>
</div>
<div class="message-input-area">
<textarea placeholder="输入消息..." rows="3"></textarea>
<div class="input-actions">
<button class="btn btn-small">📎</button>
<button class="btn btn-small">😊</button>
<button class="btn btn-primary">发送</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Message'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.messages-layout {
display: flex;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
height: 600px;
overflow: hidden;
}
.messages-sidebar {
width: 300px;
border-right: 1px solid #e4e7ed;
display: flex;
flex-direction: column;
}
.message-filters {
display: flex;
border-bottom: 1px solid #e4e7ed;
.filter-btn {
flex: 1;
padding: 16px;
border: none;
background-color: #fff;
color: #606266;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
border-bottom: 2px solid transparent;
&:hover {
color: #409eff;
}
&.active {
color: #409eff;
border-bottom-color: #409eff;
}
}
}
.message-threads {
flex: 1;
overflow-y: auto;
}
.thread-item {
display: flex;
align-items: center;
padding: 16px;
cursor: pointer;
transition: background-color 0.3s;
border-bottom: 1px solid #f5f7fa;
&:hover {
background-color: #f5f7fa;
}
&.active {
background-color: #ecf5ff;
}
}
.thread-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #409eff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
margin-right: 12px;
}
.thread-info {
flex: 1;
overflow: hidden;
}
.thread-name {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.thread-preview {
font-size: 12px;
color: #606266;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.thread-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.thread-time {
font-size: 12px;
color: #909399;
}
.thread-badge {
background-color: #f56c6c;
color: #fff;
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
min-width: 18px;
text-align: center;
}
.messages-main {
flex: 1;
display: flex;
flex-direction: column;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e4e7ed;
background-color: #fafafa;
}
.header-info {
display: flex;
align-items: center;
gap: 12px;
}
.header-name {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.header-status {
font-size: 12px;
color: #67c23a;
display: flex;
align-items: center;
&:before {
content: '';
width: 8px;
height: 8px;
background-color: #67c23a;
border-radius: 50%;
margin-right: 4px;
}
}
.header-actions {
display: flex;
gap: 8px;
}
.btn-small {
padding: 8px;
font-size: 14px;
border: 1px solid #dcdfe6;
background-color: #fff;
color: #606266;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #409eff;
border-color: #c6e2ff;
}
}
.messages-content {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f5f7fa;
display: flex;
flex-direction: column;
gap: 12px;
}
.message {
display: flex;
margin-bottom: 12px;
&.sent {
justify-content: flex-end;
}
&.received {
justify-content: flex-start;
}
}
.message-bubble {
max-width: 60%;
padding: 12px 16px;
border-radius: 12px;
position: relative;
.message-text {
font-size: 14px;
color: #303133;
line-height: 1.5;
margin-bottom: 4px;
}
.message-time {
font-size: 12px;
color: #909399;
text-align: right;
}
}
.message.sent .message-bubble {
background-color: #409eff;
color: #fff;
border-bottom-right-radius: 4px;
.message-text {
color: #fff;
}
.message-time {
color: rgba(255, 255, 255, 0.8);
}
}
.message.received .message-bubble {
background-color: #fff;
color: #303133;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.message-input-area {
padding: 16px 20px;
border-top: 1px solid #e4e7ed;
background-color: #fafafa;
display: flex;
flex-direction: column;
gap: 12px;
}
.message-input-area textarea {
width: 100%;
padding: 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #606266;
resize: none;
outline: none;
transition: border-color 0.3s;
&:focus {
border-color: #409eff;
}
}
.input-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.input-actions .btn-primary {
padding: 8px 16px;
font-size: 14px;
}
</style>

184
src/views/Notification.vue Normal file
View File

@@ -0,0 +1,184 @@
<template>
<div class="page-container">
<h2 class="page-title">🔔 通知中心</h2>
<div class="page-content">
<div class="notifications-list">
<div class="notification-item unread">
<div class="notification-icon">📢</div>
<div class="notification-content">
<div class="notification-title">系统公告</div>
<div class="notification-text">新功能已上线快来体验吧</div>
<div class="notification-time">2分钟前</div>
</div>
<div class="notification-actions">
<button class="btn btn-small">查看</button>
</div>
</div>
<div class="notification-item">
<div class="notification-icon">📦</div>
<div class="notification-content">
<div class="notification-title">订单通知</div>
<div class="notification-text">您的订单 #12345 已发货</div>
<div class="notification-time">1小时前</div>
</div>
<div class="notification-actions">
<button class="btn btn-small">查看</button>
</div>
</div>
<div class="notification-item">
<div class="notification-icon">👥</div>
<div class="notification-content">
<div class="notification-title">用户操作</div>
<div class="notification-text">张三评论了您的文章</div>
<div class="notification-time">3小时前</div>
</div>
<div class="notification-actions">
<button class="btn btn-small">查看</button>
</div>
</div>
<div class="notification-item">
<div class="notification-icon">🔧</div>
<div class="notification-content">
<div class="notification-title">系统维护</div>
<div class="notification-text">系统将于明天凌晨2点进行维护</div>
<div class="notification-time">昨天</div>
</div>
<div class="notification-actions">
<button class="btn btn-small">查看</button>
</div>
</div>
</div>
<div class="notification-stats">
<div class="stat">
<span class="stat-number">12</span>
<span class="stat-text">未读通知</span>
</div>
<div class="stat">
<span class="stat-number">89</span>
<span class="stat-text">本月通知</span>
</div>
<div class="stat">
<span class="stat-number">543</span>
<span class="stat-text">全部通知</span>
</div>
<button class="btn btn-small">标记全部已读</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Notification'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.notifications-list {
margin-bottom: 24px;
}
.notification-item {
display: flex;
align-items: flex-start;
background-color: #fff;
border-radius: 8px;
padding: 20px;
margin-bottom: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
&.unread {
border-left: 4px solid #409eff;
background-color: #ecf5ff;
}
}
.notification-icon {
font-size: 24px;
margin-right: 16px;
margin-top: 4px;
}
.notification-content {
flex: 1;
}
.notification-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.notification-text {
font-size: 14px;
color: #606266;
margin-bottom: 8px;
line-height: 1.5;
}
.notification-time {
font-size: 12px;
color: #909399;
}
.notification-actions {
margin-left: 16px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
}
.notification-stats {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.stat {
display: flex;
align-items: center;
.stat-number {
font-size: 24px;
font-weight: 700;
color: #409eff;
margin-right: 8px;
}
.stat-text {
font-size: 14px;
color: #606266;
}
}
</style>

264
src/views/Product.vue Normal file
View File

@@ -0,0 +1,264 @@
<template>
<div class="page-container">
<h2 class="page-title">📦 产品管理</h2>
<div class="page-content">
<div class="actions-bar">
<button class="btn btn-primary">添加产品</button>
<div class="search-box">
<input type="text" placeholder="搜索产品..." />
<button class="search-btn">🔍</button>
</div>
</div>
<div class="products-table-container">
<table class="products-table">
<thead>
<tr>
<th>ID</th>
<th>产品名称</th>
<th>价格</th>
<th>库存</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>智能手表</td>
<td>¥1,299</td>
<td>123</td>
<td><span class="status active">在售</span></td>
<td>
<button class="btn btn-small">编辑</button>
<button class="btn btn-small btn-danger">删除</button>
</td>
</tr>
<tr>
<td>2</td>
<td>无线耳机</td>
<td>¥799</td>
<td>456</td>
<td><span class="status active">在售</span></td>
<td>
<button class="btn btn-small">编辑</button>
<button class="btn btn-small btn-danger">删除</button>
</td>
</tr>
<tr>
<td>3</td>
<td>平板电脑</td>
<td>¥2,499</td>
<td>78</td>
<td><span class="status active">在售</span></td>
<td>
<button class="btn btn-small">编辑</button>
<button class="btn btn-small btn-danger">删除</button>
</td>
</tr>
<tr>
<td>4</td>
<td>智能音箱</td>
<td>¥599</td>
<td>0</td>
<td><span class="status inactive">缺货</span></td>
<td>
<button class="btn btn-small">编辑</button>
<button class="btn btn-small btn-danger">删除</button>
</td>
</tr>
<tr>
<td>5</td>
<td>运动手环</td>
<td>¥399</td>
<td>234</td>
<td><span class="status active">在售</span></td>
<td>
<button class="btn btn-small">编辑</button>
<button class="btn btn-small btn-danger">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<button class="page-btn">上一页</button>
<button class="page-btn active">1</button>
<button class="page-btn">2</button>
<button class="page-btn">3</button>
<button class="page-btn">4</button>
<button class="page-btn">5</button>
<button class="page-btn">下一页</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Product'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.actions-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-box {
display: flex;
align-items: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
input {
padding: 8px 12px;
border: none;
outline: none;
font-size: 14px;
width: 200px;
}
.search-btn {
padding: 8px 12px;
border: none;
background-color: #f5f7fa;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: #e4e7ed;
}
}
}
.products-table-container {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
margin-bottom: 20px;
}
.products-table {
width: 100%;
border-collapse: collapse;
thead {
background-color: #f5f7fa;
th {
padding: 16px;
text-align: left;
font-size: 14px;
font-weight: 600;
color: #303133;
border-bottom: 2px solid #e4e7ed;
}
}
tbody {
tr {
transition: background-color 0.3s;
&:hover {
background-color: #f5f7fa;
}
td {
padding: 16px;
font-size: 14px;
color: #606266;
border-bottom: 1px solid #e4e7ed;
}
}
}
}
.status {
display: inline-block;
padding: 4px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: 500;
&.active {
background-color: #f0f9eb;
color: #67c23a;
}
&.inactive {
background-color: #fef0f0;
color: #f56c6c;
}
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
margin-right: 8px;
&:last-child {
margin-right: 0;
}
&.btn-danger {
background-color: #fef0f0;
color: #f56c6c;
border-color: #fbc4c4;
&:hover {
background-color: #fde2e2;
border-color: #fbc4c4;
}
}
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
.page-btn {
padding: 8px 12px;
margin: 0 4px;
border: 1px solid #dcdfe6;
background-color: #fff;
color: #606266;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: #409eff;
border-color: #c6e2ff;
}
&.active {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
}
}
</style>

445
src/views/Setting.vue Normal file
View File

@@ -0,0 +1,445 @@
<template>
<div class="page-container">
<h2 class="page-title"> 系统设置</h2>
<div class="page-content">
<div class="settings-grid">
<div class="setting-card">
<h3>基本设置</h3>
<div class="setting-item">
<label class="setting-label">网站名称</label>
<input type="text" class="setting-input" v-model="inputValue" placeholder="输入网站名称" />
</div>
<div class="setting-item">
<label class="setting-label">网站域名</label>
<input type="text" class="setting-input" placeholder="输入网站域名" />
</div>
<div class="setting-item">
<label class="setting-label">联系邮箱</label>
<input type="email" class="setting-input" placeholder="输入联系邮箱" />
</div>
</div>
<div class="setting-card">
<h3>安全设置</h3>
<div class="setting-item">
<label class="setting-label">登录验证</label>
<div class="setting-switch">
<input type="checkbox" id="login-auth" checked />
<label for="login-auth"></label>
</div>
</div>
<div class="setting-item">
<label class="setting-label">密码强度</label>
<select class="setting-select">
<option></option>
<option selected></option>
<option></option>
</select>
</div>
<div class="setting-item">
<label class="setting-label">Session过期时间</label>
<input type="number" class="setting-input" placeholder="3600" />
</div>
</div>
<div class="setting-card">
<h3>通知设置</h3>
<div class="setting-item">
<label class="setting-label">邮件通知</label>
<div class="setting-switch">
<input type="checkbox" id="email-notify" checked />
<label for="email-notify"></label>
</div>
</div>
<div class="setting-item">
<label class="setting-label">短信通知</label>
<div class="setting-switch">
<input type="checkbox" id="sms-notify" />
<label for="sms-notify"></label>
</div>
</div>
<div class="setting-item">
<label class="setting-label">推送通知</label>
<div class="setting-switch">
<input type="checkbox" id="push-notify" checked />
<label for="push-notify"></label>
</div>
</div>
</div>
</div>
<!-- 自定义表单组件展示区域 -->
<div class="custom-form-section">
<h2 class="section-title">自定义表单组件演示</h2>
<div class="form-components-grid">
<!-- Switch 组件 -->
<div class="form-component-item">
<h3>Switch 组件</h3>
<div class="component-demo">
<CustomSwitch
v-model="switchValue"
label="启用功能"
@change="handleSwitchChange"
/>
</div>
</div>
<!-- 原生 Input 组件 -->
<div class="form-component-item">
<h3>Input 组件</h3>
<div class="component-demo">
<input
class="custom-input"
v-model="inputValue"
placeholder="请输入内容"
@input="handleInputChange"
/>
</div>
</div>
<!-- Select 组件 -->
<div class="form-component-item">
<h3>Select 组件</h3>
<div class="component-demo">
<CustomSelect
v-model="selectValue"
:options="selectOptions"
placeholder="请选择选项"
:loading="selectLoading"
@change="handleSelectChange"
/>
</div>
</div>
<!-- RadioGroup 组件 -->
<div class="form-component-item">
<h3>RadioGroup 组件</h3>
<div class="component-demo">
<CustomRadioGroup
v-model="radioValue"
:options="radioOptions"
label="选择类型"
@change="handleRadioChange"
/>
</div>
</div>
<!-- Textarea 组件 -->
<div class="form-component-item full-width">
<h3>Textarea 组件</h3>
<div class="component-demo">
<CustomTextarea
v-model="textareaValue"
placeholder="请输入详细内容..."
:maxlength="100"
:showWordLimit="true"
@input="handleTextareaChange"
>
<template #left>
<span>左下方slot</span>
</template>
<template #right>
<span>右下方slot</span>
</template>
</CustomTextarea>
</div>
</div>
</div>
</div>
<div class="actions-footer">
<button class="btn btn-primary">保存设置</button>
<button class="btn">重置</button>
</div>
</div>
</div>
</template>
<script>
// 导入自定义表单组件
import CustomSwitch from '../components/form/Switch.vue'
import CustomSelect from '../components/form/Select.vue'
import CustomRadioGroup from '../components/form/RadioGroup.vue'
import CustomTextarea from '../components/form/Textarea.vue'
export default {
name: 'Setting',
components: {
CustomSwitch,
CustomSelect,
CustomRadioGroup,
CustomTextarea
},
data() {
return {
// Switch 组件数据
switchValue: true,
// Input 组件数据
inputValue: '',
// Select 组件数据
selectValue: '',
selectOptions: [
{ value: 'option1', label: '选项一' },
{ value: 'option2', label: '选项二' },
{ value: 'option3', label: '选项三' },
{ value: 'option4', label: '选项四' },
{ value: 'option5', label: '选项五' }
],
selectLoading: false,
// RadioGroup 组件数据
radioValue: 'type1',
radioOptions: [
{ value: 'type1', label: '类型一' },
{ value: 'type2', label: '类型二' },
{ value: 'type3', label: '类型三' }
],
// Textarea 组件数据
textareaValue: ''
}
},
methods: {
// Switch 组件事件
handleSwitchChange(value) {
console.log('Switch 状态改变:', value)
},
// Input 组件事件
handleInputChange(e) {
console.log('Input 输入:', e.target.value)
},
// Select 组件事件
handleSelectChange(value, option) {
console.log('Select 选择:', value, option)
},
// RadioGroup 组件事件
handleRadioChange(value, option) {
console.log('Radio 选择:', value, option)
},
// Textarea 组件事件
handleTextareaChange(e) {
console.log('Textarea 输入:', e.target.value)
}
}
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
background-color: @bg-primary;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.setting-card {
background-color: @bg-secondary;
border: 1px solid @border-color;
border-radius: 8px;
padding: 24px;
box-shadow: @shadow-light;
transition: @transition;
&:hover {
box-shadow: @shadow;
}
h3 {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid @border-color;
}
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.setting-label {
font-size: 14px;
font-weight: 500;
min-width: 100px;
}
.setting-input,
.setting-select {
padding: 8px 12px;
background-color: @bg-tertiary;
color: @text-primary;
border: 1px solid @border-color;
border-radius: 4px;
font-size: 14px;
width: 200px;
outline: none;
transition: @transition;
&:focus {
border-color: @primary-color;
background-color: @bg-secondary;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
&::placeholder {
color: @text-placeholder;
}
}
.setting-select {
cursor: pointer;
background-color: @bg-tertiary;
&:hover {
border-color: @primary-hover;
}
}
.setting-switch {
position: relative;
display: inline-block;
width: 40px;
height: 20px;
input {
opacity: 0;
width: 0;
height: 0;
&:checked + label {
background-color: @primary-color;
}
&:checked + label:before {
transform: translateX(20px);
}
}
label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: @border-color;
transition: @transition;
border-radius: 20px;
&:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 2px;
bottom: 2px;
background-color: @bg-primary;
transition: @transition;
border-radius: 50%;
}
&:hover {
background-color: @border-color-light;
}
}
}
.actions-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 自定义表单组件展示区域样式 */
.custom-form-section {
margin-top: 32px;
padding: 24px;
background-color: @bg-secondary;
border: 1px solid @border-color;
border-radius: 8px;
box-shadow: @shadow-light;
}
.section-title {
font-size: 20px;
font-weight: 600;
margin-bottom: 24px;
}
.form-components-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
}
.form-component-item {
background-color: @bg-tertiary;
border: 1px solid @border-color;
border-radius: 8px;
padding: 20px;
box-shadow: @shadow-light;
transition: @transition;
&:hover {
background-color: @bg-hover;
box-shadow: @shadow;
}
&.full-width {
grid-column: 1 / -1;
}
h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
}
.component-demo {
width: 100%;
/* 确保组件占满宽度 */
.custom-input,
:deep(.custom-select),
:deep(.custom-textarea),
:deep(.custom-radio-group) {
width: 100%;
}
/* Textarea 组件特殊处理 */
:deep(.custom-textarea) {
min-height: 120px;
}
}
}
</style>

218
src/views/User.vue Normal file
View File

@@ -0,0 +1,218 @@
<template>
<div class="page-container">
<h2 class="page-title">👥 用户管理</h2>
<div class="page-content">
<div class="users-grid">
<div class="user-card">
<div class="user-avatar">A</div>
<div class="user-info">
<div class="user-name">张三</div>
<div class="user-email">zhangsan@example.com</div>
<div class="user-role">管理员</div>
</div>
<div class="user-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small btn-danger">禁用</button>
</div>
</div>
<div class="user-card">
<div class="user-avatar">B</div>
<div class="user-info">
<div class="user-name">李四</div>
<div class="user-email">lisi@example.com</div>
<div class="user-role">普通用户</div>
</div>
<div class="user-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small btn-danger">禁用</button>
</div>
</div>
<div class="user-card">
<div class="user-avatar">C</div>
<div class="user-info">
<div class="user-name">王五</div>
<div class="user-email">wangwu@example.com</div>
<div class="user-role">普通用户</div>
</div>
<div class="user-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small btn-danger">禁用</button>
</div>
</div>
<div class="user-card">
<div class="user-avatar">D</div>
<div class="user-info">
<div class="user-name">赵六</div>
<div class="user-email">zhaoliu@example.com</div>
<div class="user-role">普通用户</div>
</div>
<div class="user-actions">
<button class="btn btn-small">查看</button>
<button class="btn btn-small btn-danger">禁用</button>
</div>
</div>
</div>
<div class="user-stats">
<div class="stat-box">
<div class="stat-value">2,345</div>
<div class="stat-desc">总用户数</div>
</div>
<div class="stat-box">
<div class="stat-value">1,678</div>
<div class="stat-desc">活跃用户</div>
</div>
<div class="stat-box">
<div class="stat-value">456</div>
<div class="stat-desc">新增用户</div>
</div>
<div class="stat-box">
<div class="stat-value">211</div>
<div class="stat-desc">禁用用户</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'User'
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: #303133;
margin-bottom: 24px;
}
.page-content {
width: 100%;
}
.users-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.user-card {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
}
.user-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
background-color: #409eff;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 600;
margin-right: 16px;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 4px;
}
.user-email {
font-size: 14px;
color: #606266;
margin-bottom: 4px;
}
.user-role {
font-size: 12px;
color: #909399;
background-color: #f5f7fa;
padding: 2px 8px;
border-radius: 10px;
display: inline-block;
}
.user-actions {
display: flex;
flex-direction: column;
gap: 8px;
}
.btn-small {
padding: 6px 12px;
font-size: 12px;
&.btn-danger {
background-color: #fef0f0;
color: #f56c6c;
border-color: #fbc4c4;
&:hover {
background-color: #fde2e2;
border-color: #fbc4c4;
}
}
}
.user-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.stat-box {
background-color: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
text-align: center;
transition: transform 0.3s, box-shadow 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #409eff;
margin-bottom: 8px;
}
.stat-desc {
font-size: 14px;
color: #606266;
}
</style>

View File

@@ -0,0 +1,366 @@
<template>
<div class="model-select" :class="showOptions ? 'show-options': ''">
<div class="label" @click="toggleOption">
<div class="label-content">
<div class="label-content__icon"></div>
<div class="label-content__text">齐蜂AI大模型</div>
</div>
<div class="label-arrow"></div>
</div>
<div v-show="showOptions" class="options">
<div class="options__left">
<div
v-for="item in leftOptions"
class="options__left-item"
:class="leftValue === item.value ? 'left-choosed' : ''"
v-bind:key="item.value"
@click="handleLeftOptionClick(item)"
>
<div class="options__left-item__icon" :class="`options__left-item__icon__${item.value}`"></div>
<div class="options__left-item__text">
{{ item.label }}
</div>
</div>
</div>
<div class="options__right">
<div
class="options__right-item"
v-for="item in rightOptons"
:class="rightValue === item.value ? 'right-choosed' : ''"
v-bind:key="item.value"
@click="handleRightOptionClick(item)"
>
<div :style="`background-image: url('${item.icon}')`" class="options__right-item__icon"></div>
<div class="options__right-item__line options__right-item__line-1">
<div class="options__right-item__title">{{ item.label }}</div>
<div v-for="tagItem in item.tags" class="options__right-item__tag" v-bind:key="tagItem">
{{ tagItem }}
</div>
</div>
<div class="options__right-item__line options__right-item__line-2">
<div class="options__right-item__desc">{{ item.desc }}</div>
</div>
<div class="options__right-item__line options__right-item__line-3">
<div class="options__right-item__cost-icon"></div>
<div class="options__right-item__cost-text">{{ item.cost }}</div>
<div class="options__right-item__split"></div>
<div class="options__right-item__time-icon"></div>
<div class="options__right-item__time-text">{{ item.time }}s</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'modelSelect',
data() {
return {
showOptions: false,
leftValue: 'text',
leftOptions: [
{
value: 'text',
label: '文生图',
},
{
value: 'image',
label: '图生图',
}
],
rightValue: 'qifeng1',
rightOptons: [
{
value: 'qifeng1',
icon: 'https://www.fofba.com/wp-content/uploads/2025/07/1752564018-u41568515953102493196fm253fmtautoapp138fJPEG.webp',
label: '齐蜂AI大模型1',
tags: ['文生图', '图生图'],
desc: '齐蜂团队最新的AI模型25.12融合加速模型',
cost: 4,
time: 12
},
{
value: 'qifeng2',
icon: 'https://www.fofba.com/wp-content/uploads/2025/07/1752564018-u41568515953102493196fm253fmtautoapp138fJPEG.webp',
label: '齐蜂AI大模型2',
tags: ['文生图', '图生图'],
desc: '齐蜂团队最新的AI模型25.12融合加速模型',
cost: 4,
time: 12
},
{
value: 'qifeng3',
icon: 'https://www.fofba.com/wp-content/uploads/2025/07/1752564018-u41568515953102493196fm253fmtautoapp138fJPEG.webp',
label: '齐蜂AI大模型3',
tags: ['文生图', '图生图'],
desc: '齐蜂团队最新的AI模型25.12融合加速模型',
cost: 4,
time: 12
},
{
value: 'qifeng4',
icon: 'https://www.fofba.com/wp-content/uploads/2025/07/1752564018-u41568515953102493196fm253fmtautoapp138fJPEG.webp',
label: '齐蜂AI大模型4',
tags: ['文生图', '图生图'],
desc: '齐蜂团队最新的AI模型25.12融合加速模型',
cost: 4,
time: 12
},
]
}
},
methods: {
toggleOption() {
this.showOptions = !this.showOptions
},
handleLeftOptionClick (item) {
const { value } = item || {};
if (value) {
this.leftValue = value;
}
},
handleRightOptionClick (item) {
const { value } = item || {};
if (value) {
this.rightValue = value;
}
},
}
}
</script>
<style lang="less" scoped>
.glass {
backdrop-filter: blur(30px);
background: rgba(162, 162, 162, 0.2);
}
.model-select {
width: 196px;
height: 46px;
flex-shrink: 0;
position: relative;
.label {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px 10px 18px;
.glass();
width: 196px;
height: 46px;
border-radius: 23px;
transition: all 0.3s ease-in-out;
&-content {
display: flex;
justify-content: flex-start;
align-items: center;
&__icon {
width: 24px;
height: 24px;
background: #FFFFFF;
margin-right: 8px;
transition: all 0.3s ease-in-out;
}
&__text {
font-family: Source Han Sans CN, Source Han Sans CN;
font-weight: bold;
font-size: 14px;
color: #FFFFFF;
transition: all 0.3s ease-in-out;
}
}
&-arrow {
width: 19px;
height: 19px;
background: #FFFFFF;
transform: rotate(0deg);
transition: all 0.3s ease-in-out;
}
}
&.show-options {
.label {
background: #FFFFFF;
.label-content {
&__icon {
background: #000000;
transition: all 0.3s ease-in-out;
}
&__text {
color: #000000;
transition: all 0.3s ease-in-out;
}
}
.label-arrow {
background: #000000;
transform: rotate(180deg);
transition: all 0.3s ease-in-out;
}
}
}
.options {
position: absolute;
left: 0;
top: 56px;
width: 1236px;
height: 434px;
background: #313131;
box-shadow: 0px 0px 6px 1px rgba(0,0,0,0.5);
border-radius: 12px 12px 12px 12px;
border: 1px solid #4E4E4E;
transition: all 0.3s ease-in-out;
padding: 0 20px;
display: flex;
justify-content: flex-start;
align-items: flex-start;
&__left {
width: 106px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
margin-right: 30px;
padding: 20px 0;
&-item {
width: 106px;
height: 72px;
background: #5A5A5A;
border-radius: 8px 8px 8px 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 10px;
opacity: 0.5;
transition: all 0.3s ease-in-out;
&__icon {
width: 30px;
height: 30px;
.img-bg();
margin-bottom: 6px;
&__text {
background-image: url('../../../../assets/imgs/index/options-image.png');
}
&__image {
background-image: url('../../../../assets/imgs/index/options-image.png');
}
}
&__text {
font-weight: bold;
font-size: 14px;
color: #FFFFFF;
}
&.left-choosed {
opacity: 1;
background: #8E67A7;
transition: all 0.3s ease-in-out;
}
}
}
&__right {
display: flex;
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
max-height: 434px;
overflow-y: scroll;
padding: 20px 0;
&-item {
width: 340px;
background: #5A5A5A;
border-radius: 8px 8px 8px 8px;
position: relative;
padding: 15px 0 10px 44px;
margin: 0 10px 10px 0;
transition: all 0.3s ease-in-out;
&__icon {
width: 24px;
height: 24px;
.img-bg();
position: absolute;
left: 11px;
top: 13px;
}
&__line {
display: flex;
justify-content: flex-start;
align-items: center;
&-1 {
margin-bottom: 8px;
}
&-2 {
margin-bottom: 4px;
}
}
&__title {
font-size: 14px;
color: #FFFFFF;
margin-right: 10px;
}
&__tag {
width: 49px;
height: 18px;
background: #747474;
color: #404040;
border-radius: 4px;
font-size: 11px;
text-align: center;
line-height: 18px;
margin-right: 6px;
transition: all 0.3s ease-in-out;
}
&__desc {
font-weight: 400;
font-size: 14px;
color: #8A8A8A;
transition: all 0.3s ease-in-out;
}
&__cost-icon , &__time-icon{
width: 24px;
height: 24px;
background: white;
}
&__cost-icon{
margin-right: 3px;
}
&__time-icon{
margin-right: 2px;
margin-left: 6px;
}
&__cost-text {
font-weight: 400;
font-size: 14px;
color: #8A8A8A;
margin-right: 8px;
}
&__split {
width: 1px;
height: 14px;
background: #8A8A8A;
}
&__time-text {
font-weight: 400;
font-size: 14px;
color: #8A8A8A;
}
&.right-choosed {
background: #8E67A7;
transition: all 0.3s ease-in-out;
.options__right-item {
&__desc {
color: #B695CE;
transition: all 0.3s ease-in-out;
}
&__tag {
background: #FFFFFF;
color: #8E67A7;
transition: all 0.3s ease-in-out;
}
}
}
}
}
}
}
</style>

14
vite.config.js Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true,
additionalData: '@import "./src/styles/theme.less";'
}
}
}
})