You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
4.4 KiB
169 lines
4.4 KiB
<!--
|
|
LangSelect:第三方服务「支持语言(多选)/默认语言(单选)」选择器。
|
|
- multiple=true:值为逗号分隔的 BCP-47 码(如 "zh-CN,en-US"),展示已选 chips + 展开勾选面板。
|
|
- multiple=false:值为单个 BCP-47 码,普通下拉。
|
|
可传 options 限定候选(如默认语言只在已选支持语言里选),不传则用 LANG_OPTIONS 全集。
|
|
-->
|
|
<template>
|
|
<div class="lang-select">
|
|
<!-- 多选:已选 chips + 展开勾选面板 -->
|
|
<template v-if="multiple">
|
|
<div class="lang-chips" :class="{ 'lang-disabled': disabled }">
|
|
<span v-for="code in selected" :key="code" class="lang-chip">
|
|
{{ code }}<span class="lang-chip-name">{{ nameOf(code) }}</span>
|
|
<button type="button" class="lang-x" @click="toggle(code)">×</button>
|
|
</span>
|
|
<span v-if="!selected.length" class="lang-empty">未选择语言</span>
|
|
</div>
|
|
<details v-if="!disabled" class="lang-dd">
|
|
<summary class="lang-dd-sum">+ 选择语言(已选 {{ selected.length }})</summary>
|
|
<div class="lang-dd-panel">
|
|
<label v-for="o in opts" :key="o.code" class="lang-opt">
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-xs checkbox-primary"
|
|
:checked="selected.includes(o.code)"
|
|
@change="toggle(o.code)"
|
|
/>
|
|
<span class="lang-opt-code">{{ o.code }}</span>
|
|
<span class="lang-opt-name">{{ o.name }}</span>
|
|
</label>
|
|
</div>
|
|
</details>
|
|
</template>
|
|
|
|
<!-- 单选:下拉 -->
|
|
<select v-else class="select select-bordered select-sm w-full text-xs" :value="modelValue" :disabled="disabled" @change="onSingle">
|
|
<option value="">(未设置)</option>
|
|
<option v-for="o in opts" :key="o.code" :value="o.code">{{ o.code }} · {{ o.name }}</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { LANG_OPTIONS, langName, type LangOption } from '~/composables/useLangOptions'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
modelValue: string
|
|
multiple?: boolean
|
|
disabled?: boolean
|
|
options?: LangOption[]
|
|
}>(),
|
|
{ multiple: false, disabled: false, options: undefined },
|
|
)
|
|
|
|
const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>()
|
|
|
|
const opts = computed(() => (props.options && props.options.length ? props.options : LANG_OPTIONS))
|
|
const selected = computed(() =>
|
|
(props.modelValue || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
)
|
|
|
|
function nameOf(code: string) {
|
|
return langName(code)
|
|
}
|
|
|
|
function toggle(code: string) {
|
|
if (props.disabled) return
|
|
const cur = selected.value.slice()
|
|
const i = cur.indexOf(code)
|
|
if (i >= 0) cur.splice(i, 1)
|
|
else cur.push(code)
|
|
emit('update:modelValue', cur.join(','))
|
|
}
|
|
|
|
function onSingle(e: Event) {
|
|
emit('update:modelValue', (e.target as HTMLSelectElement).value)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.lang-select {
|
|
font-size: 12px;
|
|
}
|
|
.lang-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
margin-bottom: 4px;
|
|
min-height: 22px;
|
|
}
|
|
.lang-disabled {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
.lang-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 2px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 11px;
|
|
background: #ede9fe;
|
|
color: #6d28d9;
|
|
padding: 1px 6px;
|
|
border-radius: 9999px;
|
|
}
|
|
.lang-chip-name {
|
|
color: #a78bfa;
|
|
margin-left: 3px;
|
|
}
|
|
.lang-x {
|
|
margin-left: 3px;
|
|
color: #a78bfa;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
.lang-x:hover {
|
|
color: #6d28d9;
|
|
}
|
|
.lang-empty {
|
|
font-size: 11px;
|
|
color: #94a3b8;
|
|
}
|
|
.lang-dd {
|
|
position: relative;
|
|
}
|
|
.lang-dd-sum {
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
color: #7c3aed;
|
|
list-style: none;
|
|
user-select: none;
|
|
}
|
|
.lang-dd-sum::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
.lang-dd-panel {
|
|
position: absolute;
|
|
z-index: 30;
|
|
margin-top: 4px;
|
|
background: #fff;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 8px;
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.14);
|
|
padding: 6px;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
width: 230px;
|
|
}
|
|
.lang-opt {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 3px 6px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
}
|
|
.lang-opt:hover {
|
|
background: #f1f5f9;
|
|
}
|
|
.lang-opt-code {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
color: #334155;
|
|
}
|
|
.lang-opt-name {
|
|
color: #94a3b8;
|
|
}
|
|
</style>
|
|
|