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

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