- 云产品选择器新增地区标签页、分组数量及全部选项,自动跟随当前线路所属地区 - 将线路和型号改为紧凑选择按钮,精简型号名称的公共前缀并保留完整名称提示 - 物理服务器目录改为规格列表,支持行内展开、收起线路详情及进入定制页 - 新增商品起价升降序排序,将无有效价格的商品置于末尾 - 将规格筛选改为下拉选项,移动端支持展开更多筛选并显示已选条件数量 - 统一云服务器与物理服务器的横向配置表单,优化磁盘、带宽、镜像选择及移动端布局 - 限制桌面端配置汇总栏高度,使清单内部滚动并保持价格和购买按钮可见 - 统一产品分类标签页与胶囊按钮样式,补充查看全部产品入口 - 将产品选择器和物理服务器目录重构为 script setup、声明式模板及 scoped 样式,删除旧样式文件与相关引用
This commit is contained in:
12 files changed
+2121
-2312
No files matched your search
@@ -1,453 +1,407 @@
|
||||
<script>
|
||||
<script setup>
|
||||
/**
|
||||
* 云商品分组与分页选择器:groups/products/库存文案由父级提供,不直接请求接口。
|
||||
* 分组、商品、重试和分页分别发出意图事件;disabled 将购买提交期间的选择锁定。
|
||||
* 分组按名称归入地区标签页;分组、商品、重试和分页分别发出意图事件;disabled 将购买提交期间的选择锁定。
|
||||
*/
|
||||
import "~/assets/styles/ProductGroupLabel.B1kWpFA-.css";
|
||||
import "~/assets/styles/CloudProductPicker.CAm9rmUl.css";
|
||||
import { toDisplayString } from "vue";
|
||||
import { renderList } from "vue";
|
||||
import { useId } from "vue";
|
||||
import { normalizeClass } from "vue";
|
||||
import { createBlock } from "vue";
|
||||
import { withCtx } from "vue";
|
||||
import { createCommentVNode } from "vue";
|
||||
import { createElementBlock } from "vue";
|
||||
import { Fragment } from "vue";
|
||||
import { createElementVNode } from "vue";
|
||||
import { createTextVNode } from "vue";
|
||||
import { unref } from "vue";
|
||||
import { defineComponent } from "vue";
|
||||
import { createVNode } from "vue";
|
||||
import { openBlock } from "vue";
|
||||
import { computed, ref, useId, watch } from "vue";
|
||||
import { NuxtLink } from "#components";
|
||||
import button from "openvue/button";
|
||||
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
|
||||
import message from "openvue/message";
|
||||
import { n as product_name_n } from "~/lib/product-name.js";
|
||||
import paginator from "openvue/paginator";
|
||||
import skeleton from "openvue/skeleton";
|
||||
import ProductGroupLabel from "~/components/ProductGroupLabel.vue";
|
||||
var C = {
|
||||
class: `cloud-product-picker`,
|
||||
},
|
||||
w = [`aria-labelledby`],
|
||||
T = [`id`],
|
||||
E = {
|
||||
class: `zone-options`,
|
||||
role: `group`,
|
||||
"aria-label": `产品专区`,
|
||||
},
|
||||
D = [`aria-pressed`, `disabled`, `onClick`],
|
||||
O = {
|
||||
key: 2,
|
||||
class: `catalog-empty`,
|
||||
},
|
||||
k = [`aria-labelledby`],
|
||||
A = [`id`],
|
||||
j = {
|
||||
key: 2,
|
||||
class: `catalog-empty`,
|
||||
},
|
||||
M = {
|
||||
key: 3,
|
||||
class: `product-options`,
|
||||
role: `group`,
|
||||
"aria-label": `选择云服务器产品`,
|
||||
},
|
||||
N = [`aria-pressed`, `disabled`, `onClick`],
|
||||
P = {
|
||||
key: 0,
|
||||
},
|
||||
F = {
|
||||
key: 1,
|
||||
},
|
||||
I = Object.assign(
|
||||
_plugin_vue_export_helper_t(
|
||||
defineComponent({
|
||||
__name: `CloudProductPicker`,
|
||||
props: {
|
||||
groups: {},
|
||||
activeGroup: {},
|
||||
products: {},
|
||||
selectedId: {},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
},
|
||||
productLoading: {
|
||||
type: Boolean,
|
||||
},
|
||||
error: {},
|
||||
errorSource: {},
|
||||
total: {},
|
||||
page: {},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
soldOut: {
|
||||
type: Function,
|
||||
},
|
||||
stockLabel: {
|
||||
type: Function,
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
`choose-group`,
|
||||
`choose-product`,
|
||||
`retry-groups`,
|
||||
`retry-products`,
|
||||
`paginate`,
|
||||
],
|
||||
setup(f, { emit: _ }) {
|
||||
let I = _,
|
||||
L = useId();
|
||||
return (n, _) => {
|
||||
let R = skeleton,
|
||||
z = button,
|
||||
B = message,
|
||||
V = ProductGroupLabel,
|
||||
H = NuxtLink,
|
||||
U = paginator;
|
||||
return (
|
||||
openBlock(),
|
||||
createElementBlock(`div`, C, [
|
||||
createElementVNode(
|
||||
`section`,
|
||||
{
|
||||
class: `selection-section`,
|
||||
"aria-labelledby": `${unref(L)}-lines-title`,
|
||||
},
|
||||
[
|
||||
createElementVNode(
|
||||
`h3`,
|
||||
{
|
||||
id: `${unref(L)}-lines-title`,
|
||||
},
|
||||
`选择线路`,
|
||||
8,
|
||||
T,
|
||||
),
|
||||
f.loading
|
||||
? (openBlock(),
|
||||
createBlock(R, {
|
||||
key: 0,
|
||||
height: `110px`,
|
||||
}))
|
||||
: (openBlock(),
|
||||
createElementBlock(
|
||||
Fragment,
|
||||
{
|
||||
key: 1,
|
||||
},
|
||||
[
|
||||
f.error && f.errorSource === `groups`
|
||||
? (openBlock(),
|
||||
createBlock(
|
||||
B,
|
||||
{
|
||||
key: 0,
|
||||
severity: `error`,
|
||||
closable: !1,
|
||||
},
|
||||
{
|
||||
default: withCtx(() => [
|
||||
createTextVNode(
|
||||
toDisplayString(f.error),
|
||||
1,
|
||||
),
|
||||
createVNode(
|
||||
z,
|
||||
{
|
||||
label: `重新加载线路`,
|
||||
severity: `secondary`,
|
||||
text: ``,
|
||||
disabled: f.disabled,
|
||||
onClick: (_[0] ||= (e) =>
|
||||
I(`retry-groups`)),
|
||||
},
|
||||
null,
|
||||
8,
|
||||
[`disabled`],
|
||||
),
|
||||
]),
|
||||
_: 1,
|
||||
},
|
||||
))
|
||||
: f.groups.length
|
||||
? (openBlock(),
|
||||
createElementBlock(
|
||||
Fragment,
|
||||
{
|
||||
key: 1,
|
||||
},
|
||||
[
|
||||
(_[3] ||= createElementVNode(
|
||||
`div`,
|
||||
{
|
||||
class: `selection-label`,
|
||||
},
|
||||
`产品专区`,
|
||||
-1,
|
||||
)),
|
||||
createElementVNode(`div`, E, [
|
||||
(openBlock(!0),
|
||||
createElementBlock(
|
||||
Fragment,
|
||||
null,
|
||||
renderList(
|
||||
f.groups,
|
||||
(e) => (
|
||||
openBlock(),
|
||||
createElementBlock(
|
||||
`button`,
|
||||
{
|
||||
key: e.id,
|
||||
type: `button`,
|
||||
class: normalizeClass([
|
||||
`zone-option`,
|
||||
{
|
||||
active:
|
||||
f.activeGroup === e.id,
|
||||
},
|
||||
]),
|
||||
"aria-pressed":
|
||||
f.activeGroup === e.id,
|
||||
disabled: f.disabled,
|
||||
onClick: (t) =>
|
||||
I(`choose-group`, e.id),
|
||||
},
|
||||
[
|
||||
createVNode(
|
||||
V,
|
||||
{
|
||||
name: e.name,
|
||||
},
|
||||
null,
|
||||
8,
|
||||
[`name`],
|
||||
),
|
||||
],
|
||||
10,
|
||||
D,
|
||||
)
|
||||
),
|
||||
),
|
||||
128,
|
||||
)),
|
||||
]),
|
||||
],
|
||||
64,
|
||||
))
|
||||
: (openBlock(),
|
||||
createElementBlock(`div`, O, [
|
||||
(_[5] ||= createElementVNode(
|
||||
`i`,
|
||||
{
|
||||
class: `pi pi-box`,
|
||||
"aria-hidden": `true`,
|
||||
},
|
||||
null,
|
||||
-1,
|
||||
)),
|
||||
(_[6] ||= createElementVNode(
|
||||
`p`,
|
||||
null,
|
||||
`暂未提供可选的云服务器线路`,
|
||||
-1,
|
||||
)),
|
||||
createVNode(
|
||||
H,
|
||||
{
|
||||
to: `/products`,
|
||||
},
|
||||
{
|
||||
default: withCtx(() => [
|
||||
...(_[4] ||= [
|
||||
createTextVNode(`浏览产品`, -1),
|
||||
]),
|
||||
]),
|
||||
_: 1,
|
||||
},
|
||||
),
|
||||
])),
|
||||
],
|
||||
64,
|
||||
)),
|
||||
],
|
||||
8,
|
||||
w,
|
||||
),
|
||||
f.groups.length
|
||||
? (openBlock(),
|
||||
createElementBlock(
|
||||
`section`,
|
||||
{
|
||||
key: 0,
|
||||
class: `selection-section`,
|
||||
"aria-labelledby": `${unref(L)}-products-title`,
|
||||
},
|
||||
[
|
||||
createElementVNode(
|
||||
`h3`,
|
||||
{
|
||||
id: `${unref(L)}-products-title`,
|
||||
class: `products-title`,
|
||||
},
|
||||
`产品`,
|
||||
8,
|
||||
A,
|
||||
),
|
||||
f.productLoading && !f.products.length
|
||||
? (openBlock(),
|
||||
createBlock(R, {
|
||||
key: 0,
|
||||
height: `110px`,
|
||||
}))
|
||||
: f.error && f.errorSource === `products`
|
||||
? (openBlock(),
|
||||
createBlock(
|
||||
B,
|
||||
{
|
||||
key: 1,
|
||||
severity: `error`,
|
||||
closable: !1,
|
||||
},
|
||||
{
|
||||
default: withCtx(() => [
|
||||
createTextVNode(
|
||||
toDisplayString(f.error),
|
||||
1,
|
||||
),
|
||||
createVNode(
|
||||
z,
|
||||
{
|
||||
label: `重新加载商品`,
|
||||
severity: `secondary`,
|
||||
text: ``,
|
||||
disabled: f.disabled,
|
||||
onClick: (_[1] ||= (e) =>
|
||||
I(`retry-products`)),
|
||||
},
|
||||
null,
|
||||
8,
|
||||
[`disabled`],
|
||||
),
|
||||
]),
|
||||
_: 1,
|
||||
},
|
||||
))
|
||||
: !f.products.length && !f.error
|
||||
? (openBlock(),
|
||||
createElementBlock(`div`, j, [
|
||||
...(_[7] ||= [
|
||||
createElementVNode(
|
||||
`p`,
|
||||
null,
|
||||
`该专区暂无可售产品`,
|
||||
-1,
|
||||
),
|
||||
]),
|
||||
]))
|
||||
: (openBlock(),
|
||||
createElementBlock(`div`, M, [
|
||||
(openBlock(!0),
|
||||
createElementBlock(
|
||||
Fragment,
|
||||
null,
|
||||
renderList(
|
||||
f.products,
|
||||
(t) => (
|
||||
openBlock(),
|
||||
createElementBlock(
|
||||
`button`,
|
||||
{
|
||||
key: t.id,
|
||||
type: `button`,
|
||||
class: normalizeClass([
|
||||
`product-option`,
|
||||
{
|
||||
active: f.selectedId === t.id,
|
||||
},
|
||||
]),
|
||||
"aria-pressed":
|
||||
f.selectedId === t.id,
|
||||
disabled:
|
||||
f.disabled || f.soldOut(t),
|
||||
onClick: (e) =>
|
||||
I(`choose-product`, t.id),
|
||||
},
|
||||
[
|
||||
createElementVNode(
|
||||
`strong`,
|
||||
null,
|
||||
toDisplayString(
|
||||
unref(product_name_n)(t.name),
|
||||
),
|
||||
1,
|
||||
),
|
||||
f.soldOut(t)
|
||||
? (openBlock(),
|
||||
createElementBlock(
|
||||
`span`,
|
||||
P,
|
||||
toDisplayString(
|
||||
f.stockLabel(t),
|
||||
),
|
||||
1,
|
||||
))
|
||||
: (openBlock(),
|
||||
createElementBlock(
|
||||
`span`,
|
||||
F,
|
||||
toDisplayString(
|
||||
f.selectedId === t.id
|
||||
? `当前配置`
|
||||
: `查看可选配置`,
|
||||
),
|
||||
1,
|
||||
)),
|
||||
],
|
||||
10,
|
||||
N,
|
||||
)
|
||||
),
|
||||
),
|
||||
128,
|
||||
)),
|
||||
])),
|
||||
f.total > 24
|
||||
? (openBlock(),
|
||||
createBlock(
|
||||
U,
|
||||
{
|
||||
key: 4,
|
||||
first: (f.page - 1) * 24,
|
||||
rows: 24,
|
||||
totalRecords: f.total,
|
||||
disabled: f.disabled,
|
||||
onPage: (_[2] ||= (e) => I(`paginate`, e)),
|
||||
},
|
||||
null,
|
||||
8,
|
||||
[`first`, `totalRecords`, `disabled`],
|
||||
))
|
||||
: createCommentVNode(``, !0),
|
||||
],
|
||||
8,
|
||||
k,
|
||||
))
|
||||
: createCommentVNode(``, !0),
|
||||
])
|
||||
);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[[`__scopeId`, `data-v-a438862d`]],
|
||||
),
|
||||
{
|
||||
__name: `CloudProductPicker`,
|
||||
},
|
||||
);
|
||||
export {};
|
||||
export default I;
|
||||
import Button from "openvue/button";
|
||||
import Message from "openvue/message";
|
||||
import Paginator from "openvue/paginator";
|
||||
import Skeleton from "openvue/skeleton";
|
||||
import ProductGroupLabel, {
|
||||
n as parseGroupLabel,
|
||||
} from "~/components/ProductGroupLabel.vue";
|
||||
import { n as displayProductName } from "~/lib/product-name.js";
|
||||
|
||||
const props = defineProps({
|
||||
groups: { type: Array, default: () => [] },
|
||||
activeGroup: {},
|
||||
products: { type: Array, default: () => [] },
|
||||
selectedId: {},
|
||||
loading: Boolean,
|
||||
productLoading: Boolean,
|
||||
error: {},
|
||||
errorSource: {},
|
||||
total: {},
|
||||
page: {},
|
||||
disabled: Boolean,
|
||||
soldOut: Function,
|
||||
stockLabel: Function,
|
||||
});
|
||||
const emit = defineEmits([
|
||||
"choose-group",
|
||||
"choose-product",
|
||||
"retry-groups",
|
||||
"retry-products",
|
||||
"paginate",
|
||||
]);
|
||||
const id = useId();
|
||||
|
||||
// 按名称关键字归类;港澳台与海外优先匹配,避免“香港BGP”之类被省份规则误判。
|
||||
const REGIONS = [
|
||||
[
|
||||
"华东",
|
||||
/上海|江苏|浙江|安徽|福建|江西|山东|华东|宿迁|金华|宁波|温州|绍兴|镇江|盐城|扬州|常州|无锡|徐州|泉州|厦门|福州|芜湖|九江|济南|杭州|南京|苏州/,
|
||||
],
|
||||
["华南", /广东|广西|海南|华南|深圳|广州|佛山|东莞|海口|南宁/],
|
||||
["华中", /湖北|湖南|河南|华中|襄阳|十堰|武汉|郑州|新乡|长沙/],
|
||||
["华北", /北京|天津|河北|山西|内蒙|华北|石家庄|太原/],
|
||||
["西南", /四川|重庆|云南|贵州|西藏|西南|成都|昆明|德阳|贵阳/],
|
||||
["西北", /陕西|甘肃|青海|宁夏|新疆|西北|西安|咸阳|兰州/],
|
||||
["东北", /辽宁|吉林|黑龙江|东北|沈阳|鞍山|大连|长春|哈尔滨/],
|
||||
];
|
||||
const ABROAD = [
|
||||
["港澳台", /香港|澳门|台湾/],
|
||||
[
|
||||
"海外",
|
||||
/美国|日本|韩国|新加坡|越南|马来|泰国|菲律宾|印尼|印度|德国|英国|法国|荷兰|俄罗斯|欧洲|洛杉矶|硅谷|东京|首尔|海外/,
|
||||
],
|
||||
];
|
||||
const REGION_ORDER = [...REGIONS, ...ABROAD]
|
||||
.map(([name]) => name)
|
||||
.concat("其他");
|
||||
|
||||
function regionOf(group) {
|
||||
const name = parseGroupLabel(group.name).name;
|
||||
for (const [region, pattern] of [...ABROAD, ...REGIONS])
|
||||
if (pattern.test(name)) return region;
|
||||
return "其他";
|
||||
}
|
||||
|
||||
const regions = computed(() => {
|
||||
const buckets = new Map();
|
||||
for (const group of props.groups) {
|
||||
const region = regionOf(group);
|
||||
if (!buckets.has(region)) buckets.set(region, []);
|
||||
buckets.get(region).push(group);
|
||||
}
|
||||
return REGION_ORDER.filter((name) => buckets.has(name)).map((name) => ({
|
||||
name,
|
||||
groups: buckets.get(name),
|
||||
}));
|
||||
});
|
||||
const activeGroupRegion = computed(() => {
|
||||
const group = props.groups.find((g) => g.id === props.activeGroup);
|
||||
return group ? regionOf(group) : regions.value[0]?.name;
|
||||
});
|
||||
const chosenRegion = ref(null);
|
||||
watch(
|
||||
() => props.activeGroup,
|
||||
() => (chosenRegion.value = null),
|
||||
);
|
||||
const currentRegion = computed(
|
||||
() => chosenRegion.value ?? activeGroupRegion.value ?? "all",
|
||||
);
|
||||
const visibleGroups = computed(() =>
|
||||
currentRegion.value === "all"
|
||||
? props.groups
|
||||
: (regions.value.find((r) => r.name === currentRegion.value)?.groups ??
|
||||
props.groups),
|
||||
);
|
||||
// 同一分组下的商品名称通常共享“地区 | 运营商(”前缀,只显示差异部分,完整名称放在 title 中。
|
||||
const productLabels = computed(() => {
|
||||
const names = props.products.map((p) => displayProductName(p.name));
|
||||
const plain = { labels: names };
|
||||
if (names.length < 2) return plain;
|
||||
let prefix = names[0];
|
||||
for (const name of names)
|
||||
while (!name.startsWith(prefix)) prefix = prefix.slice(0, -1);
|
||||
const boundary = prefix.match(/^.*[((\s||·\-_/]/);
|
||||
prefix = boundary ? boundary[0] : "";
|
||||
if (prefix.length < 2 || names.some((n) => n.length <= prefix.length))
|
||||
return plain;
|
||||
const opensParen = /[((]$/.test(prefix);
|
||||
return {
|
||||
labels: names.map((name) => {
|
||||
const rest = name.slice(prefix.length).trim();
|
||||
return opensParen ? rest.replace(/[))]$/, "") : rest;
|
||||
}),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="cloud-product-picker" :aria-labelledby="`${id}-title`">
|
||||
<h2 :id="`${id}-title`" class="section-title">选择产品</h2>
|
||||
|
||||
<div class="picker-row">
|
||||
<span :id="`${id}-lines-title`" class="picker-label">线路</span>
|
||||
<div class="picker-control">
|
||||
<Skeleton v-if="loading" height="88px" />
|
||||
<Message
|
||||
v-else-if="error && errorSource === 'groups'"
|
||||
severity="error"
|
||||
:closable="false"
|
||||
>
|
||||
{{ error }}
|
||||
<Button
|
||||
label="重新加载线路"
|
||||
severity="secondary"
|
||||
text
|
||||
:disabled="disabled"
|
||||
@click="emit('retry-groups')"
|
||||
/>
|
||||
</Message>
|
||||
<template v-else-if="groups.length">
|
||||
<div
|
||||
v-if="regions.length > 1"
|
||||
class="choice-tabs"
|
||||
role="group"
|
||||
aria-label="地区"
|
||||
>
|
||||
<button
|
||||
v-for="region in regions"
|
||||
:key="region.name"
|
||||
type="button"
|
||||
class="choice-tab"
|
||||
:class="{ active: currentRegion === region.name }"
|
||||
:aria-pressed="currentRegion === region.name"
|
||||
@click="chosenRegion = region.name"
|
||||
>
|
||||
{{ region.name }}<small>{{ region.groups.length }}</small>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="choice-tab"
|
||||
:class="{ active: currentRegion === 'all' }"
|
||||
:aria-pressed="currentRegion === 'all'"
|
||||
@click="chosenRegion = 'all'"
|
||||
>
|
||||
全部<small>{{ groups.length }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="choice-chips"
|
||||
role="group"
|
||||
:aria-labelledby="`${id}-lines-title`"
|
||||
>
|
||||
<button
|
||||
v-for="group in visibleGroups"
|
||||
:key="group.id"
|
||||
type="button"
|
||||
class="choice-chip"
|
||||
:class="{ active: activeGroup === group.id }"
|
||||
:aria-pressed="activeGroup === group.id"
|
||||
:disabled="disabled"
|
||||
@click="emit('choose-group', group.id)"
|
||||
>
|
||||
<ProductGroupLabel :name="group.name" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="catalog-empty">
|
||||
<i class="pi pi-box" aria-hidden="true" />
|
||||
<p>暂未提供可选的云服务器线路</p>
|
||||
<NuxtLink to="/products">浏览产品</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="groups.length" class="picker-row">
|
||||
<span :id="`${id}-products-title`" class="picker-label">型号</span>
|
||||
<div class="picker-control">
|
||||
<Skeleton v-if="productLoading && !products.length" height="34px" />
|
||||
<Message
|
||||
v-else-if="error && errorSource === 'products'"
|
||||
severity="error"
|
||||
:closable="false"
|
||||
>
|
||||
{{ error }}
|
||||
<Button
|
||||
label="重新加载商品"
|
||||
severity="secondary"
|
||||
text
|
||||
:disabled="disabled"
|
||||
@click="emit('retry-products')"
|
||||
/>
|
||||
</Message>
|
||||
<p v-else-if="!products.length && !error" class="picker-empty">
|
||||
该专区暂无可售产品
|
||||
</p>
|
||||
<div
|
||||
v-else
|
||||
class="choice-chips"
|
||||
role="group"
|
||||
:aria-labelledby="`${id}-products-title`"
|
||||
>
|
||||
<button
|
||||
v-for="(product, index) in products"
|
||||
:key="product.id"
|
||||
type="button"
|
||||
class="choice-chip"
|
||||
:class="{ active: selectedId === product.id }"
|
||||
:aria-pressed="selectedId === product.id"
|
||||
:title="displayProductName(product.name)"
|
||||
:disabled="disabled || soldOut(product)"
|
||||
@click="emit('choose-product', product.id)"
|
||||
>
|
||||
{{ productLabels.labels[index] }}
|
||||
<small v-if="soldOut(product)">{{ stockLabel(product) }}</small>
|
||||
</button>
|
||||
</div>
|
||||
<Paginator
|
||||
v-if="total > 24"
|
||||
:first="(page - 1) * 24"
|
||||
:rows="24"
|
||||
:total-records="total"
|
||||
:disabled="disabled"
|
||||
@page="emit('paginate', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cloud-product-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
}
|
||||
.cloud-product-picker > .section-title {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.picker-row {
|
||||
display: grid;
|
||||
grid-template-columns: 88px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 8px 16px;
|
||||
}
|
||||
.picker-label {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.9375rem;
|
||||
line-height: 34px;
|
||||
}
|
||||
.picker-control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.picker-empty {
|
||||
margin: 0;
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.875rem;
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
/* 统一选择控件:与物理服务器筛选、商品目录分类使用同一套标签页与胶囊按钮规格。 */
|
||||
.choice-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
border-bottom: 1px solid var(--p-content-border-color);
|
||||
}
|
||||
.choice-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.choice-tab {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
margin-bottom: -1px;
|
||||
padding: 6px 12px 8px;
|
||||
border: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: none;
|
||||
color: var(--p-text-muted-color);
|
||||
font: inherit;
|
||||
font-size: 0.9375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.choice-tab small {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.choice-tab:hover {
|
||||
color: var(--p-text-color);
|
||||
}
|
||||
.choice-tab.active {
|
||||
border-bottom-color: var(--p-primary-color);
|
||||
color: var(--p-primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
.choice-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.choice-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
min-height: 34px;
|
||||
padding: 5px 12px;
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--p-content-background);
|
||||
color: var(--p-text-color);
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
cursor: pointer;
|
||||
}
|
||||
.choice-chip small {
|
||||
color: var(--p-text-muted-color);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.choice-chip:hover:not(:disabled) {
|
||||
border-color: var(--p-primary-color);
|
||||
}
|
||||
.choice-chip.active {
|
||||
border-color: var(--p-primary-color);
|
||||
box-shadow: inset 0 0 0 1px var(--p-primary-color);
|
||||
background: var(--p-highlight-background);
|
||||
color: var(--p-highlight-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
.choice-chip:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.choice-chip:focus-visible,
|
||||
.choice-tab:focus-visible {
|
||||
outline: 2px solid var(--p-primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.choice-chip :deep(.product-group-badge) {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
opacity: 0.6;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
.catalog-empty {
|
||||
padding: 28px;
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--p-content-background);
|
||||
color: var(--p-text-muted-color);
|
||||
text-align: center;
|
||||
}
|
||||
.catalog-empty i {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
.catalog-empty a {
|
||||
color: var(--p-primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
@media (width < 640px) {
|
||||
.picker-row {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
.picker-label {
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user