优化移动端即梦页面

This commit is contained in:
RockYang
2025-08-07 22:27:09 +08:00
parent e456210944
commit 4e237c9560
14 changed files with 1906 additions and 1239 deletions

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>