mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-21 10:34:26 +08:00
优化移动端即梦页面
This commit is contained in:
110
web/src/components/mobile/AppCard.vue
Normal file
110
web/src/components/mobile/AppCard.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<van-cell class="app-cell">
|
||||
<div class="app-card">
|
||||
<div class="app-info">
|
||||
<div class="app-image">
|
||||
<van-image :src="app.icon" round />
|
||||
</div>
|
||||
<div class="app-detail">
|
||||
<div class="app-title">{{ app.name }}</div>
|
||||
<div class="app-desc">{{ app.hello_msg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
@click="$emit('use-role', app.id)"
|
||||
>开始对话</van-button
|
||||
>
|
||||
<van-button
|
||||
size="small"
|
||||
:type="hasRole ? 'danger' : 'success'"
|
||||
class="action-btn"
|
||||
@click="$emit('update-role', app, hasRole ? 'remove' : 'add')"
|
||||
>
|
||||
{{ hasRole ? '移出工作台' : '添加到工作台' }}
|
||||
</van-button>
|
||||
</div>
|
||||
</div>
|
||||
</van-cell>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
app: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
hasRole: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['use-role', 'update-role'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-cell {
|
||||
padding: 0;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.app-card {
|
||||
background: var(--van-cell-background);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.app-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.app-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 15px;
|
||||
|
||||
:deep(.van-image) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.app-detail {
|
||||
flex: 1;
|
||||
|
||||
.app-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
font-size: 13px;
|
||||
color: var(--van-gray-6);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
border-radius: 20px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
116
web/src/components/mobile/CustomSelect.vue
Normal file
116
web/src/components/mobile/CustomSelect.vue
Normal file
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="custom-select">
|
||||
<label v-if="label" class="block text-gray-700 font-medium mb-2">{{ label }}</label>
|
||||
<button
|
||||
@click="showPicker = true"
|
||||
class="w-full flex items-center justify-between px-4 py-3 bg-gray-50 rounded-lg border border-gray-200 hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<span class="text-gray-900">{{ selectedLabel || placeholder || '请选择' }}</span>
|
||||
<i class="iconfont icon-arrow-down text-gray-400"></i>
|
||||
</button>
|
||||
|
||||
<!-- 选择器弹窗 -->
|
||||
<div
|
||||
v-if="showPicker"
|
||||
class="fixed inset-0 z-50 flex items-end justify-center bg-black bg-opacity-50"
|
||||
@click="showPicker = false"
|
||||
>
|
||||
<div @click.stop class="bg-white rounded-t-2xl w-full max-w-md animate-slide-up">
|
||||
<div class="flex items-center justify-between p-4 border-b">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ title || '请选择' }}</h3>
|
||||
<button @click="showPicker = false" class="p-2 hover:bg-gray-100 rounded-full">
|
||||
<i class="iconfont icon-close text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
<CustomSelectOption
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:option="option"
|
||||
:selected="modelValue === option.value"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #default="slotProps">
|
||||
<slot name="option" v-bind="slotProps">
|
||||
<div>
|
||||
<span class="text-gray-900 font-medium">{{ slotProps.option.label }}</span>
|
||||
<p v-if="slotProps.option.desc" class="text-sm text-gray-500 mt-1">
|
||||
{{ slotProps.option.desc }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="slotProps.selected" class="text-blue-600">
|
||||
<i class="iconfont icon-success"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
</CustomSelectOption>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import CustomSelectOption from './CustomSelectOption.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '请选择',
|
||||
},
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
// Data
|
||||
const showPicker = ref(false)
|
||||
|
||||
// Computed
|
||||
const selectedLabel = computed(() => {
|
||||
const selected = props.options.find((option) => option.value === props.modelValue)
|
||||
return selected ? selected.label : ''
|
||||
})
|
||||
|
||||
// Methods
|
||||
const onSelect = (option) => {
|
||||
emit('update:modelValue', option.value)
|
||||
emit('change', option)
|
||||
showPicker.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slide-up 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
50
web/src/components/mobile/CustomSelectOption.vue
Normal file
50
web/src/components/mobile/CustomSelectOption.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<!--
|
||||
CustomSelectOption 组件
|
||||
Props:
|
||||
- option: 选项对象,必需,包含 label/desc/value 等属性
|
||||
- selected: 是否为当前选中项
|
||||
Emits:
|
||||
- select(option): 选中该项时触发
|
||||
Slots:
|
||||
- 默认插槽(default):用于自定义 option 内容,slotProps: { option, selected }
|
||||
示例:
|
||||
<template #option="{ option, selected }">
|
||||
<div>{{ option.label }}</div>
|
||||
<div v-if="selected">✔</div>
|
||||
</template>
|
||||
-->
|
||||
<div
|
||||
class="flex items-center justify-between p-4 hover:bg-gray-50 cursor-pointer transition-colors border-b last:border-b-0"
|
||||
@click="$emit('select', option)"
|
||||
>
|
||||
<slot :option="option" :selected="selected">
|
||||
<div>
|
||||
<span class="text-gray-900 font-medium">{{ option.label }}</span>
|
||||
<p v-if="option.desc" class="text-sm text-gray-500 mt-1">{{ option.desc }}</p>
|
||||
</div>
|
||||
<div v-if="selected" class="text-blue-600">
|
||||
<i class="iconfont icon-success"></i>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
selected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
74
web/src/components/mobile/EmptyState.vue
Normal file
74
web/src/components/mobile/EmptyState.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="empty-state">
|
||||
<van-empty :image="getImage()" :description="description" :image-size="imageSize">
|
||||
<template #bottom>
|
||||
<slot name="action">
|
||||
<van-button
|
||||
v-if="showAction"
|
||||
round
|
||||
type="primary"
|
||||
class="action-btn"
|
||||
@click="$emit('action')"
|
||||
>
|
||||
{{ actionText }}
|
||||
</van-button>
|
||||
</slot>
|
||||
</template>
|
||||
</van-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'search', // search, error, network, default
|
||||
validator: (value) => ['search', 'error', 'network', 'default'].includes(value),
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: '暂无数据',
|
||||
},
|
||||
imageSize: {
|
||||
type: [String, Number],
|
||||
default: 120,
|
||||
},
|
||||
showAction: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionText: {
|
||||
type: String,
|
||||
default: '刷新',
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(['action'])
|
||||
|
||||
// 根据类型获取对应的图标
|
||||
const getImage = () => {
|
||||
const imageMap = {
|
||||
search: 'search',
|
||||
error: 'error',
|
||||
network: 'network',
|
||||
default: 'default',
|
||||
}
|
||||
return imageMap[props.type] || 'search'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.empty-state {
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
|
||||
.action-btn {
|
||||
margin-top: 16px;
|
||||
min-width: 120px;
|
||||
height: 36px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div class="black-dialog">
|
||||
<el-dialog v-model="showDialog" :title="title" :width="width" :before-close="cancel">
|
||||
<div class="dialog-body">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<template #footer v-if="!hideFooter">
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel" style="--el-border-radius-base: 8px">{{
|
||||
cancelText
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="$emit('confirm')" v-if="!hideConfirm">{{
|
||||
confirmText
|
||||
}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Tips',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: 'auto',
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hideConfirm: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: '确定',
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消',
|
||||
},
|
||||
})
|
||||
const emits = defineEmits(['confirm', 'cancal'])
|
||||
const showDialog = ref(props.show)
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(newValue) => {
|
||||
showDialog.value = newValue
|
||||
}
|
||||
)
|
||||
const cancel = () => {
|
||||
showDialog.value = false
|
||||
emits('cancal')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.black-dialog {
|
||||
.dialog-body {
|
||||
.form {
|
||||
.form-item {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
font-family: 'Neue Montreal';
|
||||
padding: 10px 0;
|
||||
|
||||
.label {
|
||||
margin-bottom: 0.6rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
color: #ffffff;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
font-size: 1rem;
|
||||
background: none;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #8f8f8f;
|
||||
outline: none;
|
||||
transition: border-color 0.5s ease, box-shadow 0.5s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: #0f7a71;
|
||||
box-shadow: 0 0 5px #0f7a71;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-model="model"
|
||||
:placeholder="placeholder"
|
||||
:value="value"
|
||||
@change="$emit('update:value', $event)"
|
||||
style="--el-border-radius-base: 20px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "BlackSelect",
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请选择"
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
model: this.value
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<el-switch
|
||||
v-model="model"
|
||||
:size="size"
|
||||
@change="$emit('update:value', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
const props = defineProps({
|
||||
value: Boolean,
|
||||
size: {
|
||||
type: String,
|
||||
default: "default"
|
||||
}
|
||||
});
|
||||
const model = ref(props.value);
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(newValue) => {
|
||||
model.value = newValue;
|
||||
}
|
||||
);
|
||||
</script>
|
||||
Reference in New Issue
Block a user