mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix: post-merge fixes for upstream v0.1.112 integration
- Fix AccountUsageCell loadUsage() parameter type mismatch (string→object) - Add missing i18n keys for openaiMessages family mapping (en/zh, 17 keys) - Remove duplicate payment provider declarations in wire.go - Add sort_order field to PlanEditDialog.vue - Remove 10 duplicate upstream-renumbered migration files - Fix NewUserService call signatures in test files (missing 4th param) - Update api_contract_test.go snapshots for new balance_notify fields
This commit is contained in:
@@ -43,13 +43,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
||||
// Server layer ProviderSet
|
||||
server.ProviderSet,
|
||||
|
||||
// Payment providers
|
||||
payment.ProvideRegistry,
|
||||
payment.ProvideEncryptionKey,
|
||||
payment.ProvideDefaultLoadBalancer,
|
||||
service.ProvidePaymentConfigService,
|
||||
service.ProvidePaymentOrderExpiryService,
|
||||
|
||||
// Privacy client factory for OpenAI training opt-out
|
||||
providePrivacyClientFactory,
|
||||
|
||||
|
||||
@@ -53,8 +53,13 @@ func TestAPIContracts(t *testing.T) {
|
||||
"username": "alice",
|
||||
"role": "user",
|
||||
"balance": 12.5,
|
||||
"balance_notify_enabled": false,
|
||||
"balance_notify_extra_emails": null,
|
||||
"balance_notify_threshold": null,
|
||||
"balance_notify_threshold_type": "",
|
||||
"concurrency": 5,
|
||||
"status": "active",
|
||||
"total_recharged": 0,
|
||||
"allowed_groups": null,
|
||||
"created_at": "2025-01-02T03:04:05Z",
|
||||
"updated_at": "2025-01-02T03:04:05Z",
|
||||
@@ -606,7 +611,12 @@ func TestAPIContracts(t *testing.T) {
|
||||
"payment_cancel_rate_limit_max": 0,
|
||||
"payment_cancel_rate_limit_window": 0,
|
||||
"payment_cancel_rate_limit_unit": "",
|
||||
"payment_cancel_rate_limit_window_mode": ""
|
||||
"payment_cancel_rate_limit_window_mode": "",
|
||||
"balance_low_notify_enabled": false,
|
||||
"balance_low_notify_threshold": 0,
|
||||
"balance_low_notify_recharge_url": "",
|
||||
"account_quota_notify_enabled": false,
|
||||
"account_quota_notify_emails": []
|
||||
}
|
||||
}`,
|
||||
},
|
||||
@@ -699,7 +709,7 @@ func newContractDeps(t *testing.T) *contractDeps {
|
||||
RunMode: config.RunModeStandard,
|
||||
}
|
||||
|
||||
userService := service.NewUserService(userRepo, nil, nil)
|
||||
userService := service.NewUserService(userRepo, nil, nil, nil)
|
||||
apiKeyService := service.NewAPIKeyService(apiKeyRepo, userRepo, groupRepo, userSubRepo, nil, apiKeyCache, cfg)
|
||||
|
||||
usageRepo := newStubUsageLogRepo()
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestAdminAuthJWTValidatesTokenVersion(t *testing.T) {
|
||||
return &clone, nil
|
||||
},
|
||||
}
|
||||
userService := service.NewUserService(userRepo, nil, nil)
|
||||
userService := service.NewUserService(userRepo, nil, nil, nil)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.HandlerFunc(NewAdminAuthMiddleware(authService, userService, nil)))
|
||||
|
||||
@@ -41,7 +41,7 @@ func newJWTTestEnv(users map[int64]*service.User) (*gin.Engine, *service.AuthSer
|
||||
|
||||
userRepo := &stubJWTUserRepo{users: users}
|
||||
authSvc := service.NewAuthService(nil, userRepo, nil, nil, cfg, nil, nil, nil, nil, nil, nil)
|
||||
userSvc := service.NewUserService(userRepo, nil, nil)
|
||||
userSvc := service.NewUserService(userRepo, nil, nil, nil)
|
||||
mw := NewJWTAuthMiddleware(authSvc, userSvc)
|
||||
|
||||
r := gin.New()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS payment_orders (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
user_email VARCHAR(255) NOT NULL DEFAULT '',
|
||||
user_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
user_notes TEXT,
|
||||
amount DECIMAL(20,2) NOT NULL,
|
||||
pay_amount DECIMAL(20,2) NOT NULL,
|
||||
fee_rate DECIMAL(10,4) NOT NULL DEFAULT 0,
|
||||
recharge_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
payment_type VARCHAR(30) NOT NULL DEFAULT '',
|
||||
payment_trade_no VARCHAR(128) NOT NULL DEFAULT '',
|
||||
pay_url TEXT,
|
||||
qr_code TEXT,
|
||||
qr_code_img TEXT,
|
||||
order_type VARCHAR(20) NOT NULL DEFAULT 'balance',
|
||||
plan_id BIGINT,
|
||||
subscription_group_id BIGINT,
|
||||
subscription_days INT,
|
||||
provider_instance_id VARCHAR(64),
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'PENDING',
|
||||
refund_amount DECIMAL(20,2) NOT NULL DEFAULT 0,
|
||||
refund_reason TEXT,
|
||||
refund_at TIMESTAMPTZ,
|
||||
force_refund BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
refund_requested_at TIMESTAMPTZ,
|
||||
refund_request_reason TEXT,
|
||||
refund_requested_by VARCHAR(20),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
paid_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
failed_at TIMESTAMPTZ,
|
||||
failed_reason TEXT,
|
||||
client_ip VARCHAR(50) NOT NULL DEFAULT '',
|
||||
src_host VARCHAR(255) NOT NULL DEFAULT '',
|
||||
src_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_user_id ON payment_orders(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_status ON payment_orders(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_expires_at ON payment_orders(expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_created_at ON payment_orders(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_paid_at ON payment_orders(paid_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_type_paid ON payment_orders(payment_type, paid_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_orders_order_type ON payment_orders(order_type);
|
||||
@@ -1,9 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS payment_audit_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
order_id VARCHAR(64) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
detail TEXT NOT NULL DEFAULT '',
|
||||
operator VARCHAR(100) NOT NULL DEFAULT 'system',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_audit_logs_order_id ON payment_audit_logs(order_id);
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Migration 092: payment_channels table was removed before release.
|
||||
-- This file is a no-op placeholder to maintain migration numbering continuity.
|
||||
-- The payment system now uses the existing channels table (migration 081).
|
||||
SELECT 1;
|
||||
@@ -1,18 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS subscription_plans (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
group_id BIGINT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
price DECIMAL(20,2) NOT NULL,
|
||||
original_price DECIMAL(20,2),
|
||||
validity_days INT NOT NULL DEFAULT 30,
|
||||
validity_unit VARCHAR(10) NOT NULL DEFAULT 'day',
|
||||
features TEXT NOT NULL DEFAULT '',
|
||||
product_name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
for_sale BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_subscription_plans_group_id ON subscription_plans(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_subscription_plans_for_sale ON subscription_plans(for_sale);
|
||||
@@ -1,15 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS payment_provider_instances (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
provider_key VARCHAR(30) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL DEFAULT '',
|
||||
config TEXT NOT NULL,
|
||||
supported_types VARCHAR(200) NOT NULL DEFAULT '',
|
||||
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
limits TEXT NOT NULL DEFAULT '',
|
||||
refund_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_provider_instances_provider_key ON payment_provider_instances(provider_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_payment_provider_instances_enabled ON payment_provider_instances(enabled);
|
||||
@@ -1,70 +0,0 @@
|
||||
-- 096_migrate_purchase_subscription_to_custom_menu.sql
|
||||
--
|
||||
-- Migrates the legacy purchase_subscription_url setting into custom_menu_items.
|
||||
-- After migration, purchase_subscription_enabled is set to "false" and
|
||||
-- purchase_subscription_url is cleared.
|
||||
--
|
||||
-- Idempotent: skips if custom_menu_items already contains
|
||||
-- "migrated_purchase_subscription".
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_enabled text;
|
||||
v_url text;
|
||||
v_raw text;
|
||||
v_items jsonb;
|
||||
v_new_item jsonb;
|
||||
BEGIN
|
||||
-- Read legacy settings
|
||||
SELECT value INTO v_enabled
|
||||
FROM settings WHERE key = 'purchase_subscription_enabled';
|
||||
SELECT value INTO v_url
|
||||
FROM settings WHERE key = 'purchase_subscription_url';
|
||||
|
||||
-- Skip if not enabled or URL is empty
|
||||
IF COALESCE(v_enabled, '') <> 'true' OR COALESCE(TRIM(v_url), '') = '' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Read current custom_menu_items
|
||||
SELECT value INTO v_raw
|
||||
FROM settings WHERE key = 'custom_menu_items';
|
||||
|
||||
IF COALESCE(v_raw, '') = '' OR v_raw = 'null' THEN
|
||||
v_items := '[]'::jsonb;
|
||||
ELSE
|
||||
v_items := v_raw::jsonb;
|
||||
END IF;
|
||||
|
||||
-- Skip if already migrated (item with id "migrated_purchase_subscription" exists)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM jsonb_array_elements(v_items) elem
|
||||
WHERE elem ->> 'id' = 'migrated_purchase_subscription'
|
||||
) THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
-- Build the new menu item
|
||||
v_new_item := jsonb_build_object(
|
||||
'id', 'migrated_purchase_subscription',
|
||||
'label', 'Purchase',
|
||||
'icon_svg', '',
|
||||
'url', TRIM(v_url),
|
||||
'visibility', 'user',
|
||||
'sort_order', 100
|
||||
);
|
||||
|
||||
-- Append to array
|
||||
v_items := v_items || jsonb_build_array(v_new_item);
|
||||
|
||||
-- Upsert custom_menu_items
|
||||
INSERT INTO settings (key, value)
|
||||
VALUES ('custom_menu_items', v_items::text)
|
||||
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value;
|
||||
|
||||
-- Clear legacy settings
|
||||
UPDATE settings SET value = 'false' WHERE key = 'purchase_subscription_enabled';
|
||||
UPDATE settings SET value = '' WHERE key = 'purchase_subscription_url';
|
||||
|
||||
RAISE NOTICE '[migration-096] Migrated purchase_subscription_url (%) to custom_menu_items', v_url;
|
||||
END $$;
|
||||
@@ -1,51 +0,0 @@
|
||||
-- 097_fix_migrated_purchase_menu_label_icon.sql
|
||||
--
|
||||
-- Fixes the custom menu item created by migration 096: updates the label
|
||||
-- from hardcoded English "Purchase" to "充值/订阅", and sets the icon_svg
|
||||
-- to a credit-card SVG matching the sidebar CreditCardIcon.
|
||||
--
|
||||
-- Idempotent: only modifies items where id = 'migrated_purchase_subscription'.
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_raw text;
|
||||
v_items jsonb;
|
||||
v_idx int;
|
||||
v_icon text;
|
||||
v_elem jsonb;
|
||||
v_i int := 0;
|
||||
BEGIN
|
||||
SELECT value INTO v_raw
|
||||
FROM settings WHERE key = 'custom_menu_items';
|
||||
|
||||
IF COALESCE(v_raw, '') = '' OR v_raw = 'null' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
v_items := v_raw::jsonb;
|
||||
|
||||
-- Find the index of the migrated item by iterating the array
|
||||
v_idx := NULL;
|
||||
FOR v_elem IN SELECT jsonb_array_elements(v_items) LOOP
|
||||
IF v_elem ->> 'id' = 'migrated_purchase_subscription' THEN
|
||||
v_idx := v_i;
|
||||
EXIT;
|
||||
END IF;
|
||||
v_i := v_i + 1;
|
||||
END LOOP;
|
||||
|
||||
IF v_idx IS NULL THEN
|
||||
RETURN; -- item not found, nothing to fix
|
||||
END IF;
|
||||
|
||||
-- Credit card SVG (Heroicons outline, matches CreditCardIcon in AppSidebar)
|
||||
v_icon := '<svg fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"/></svg>';
|
||||
|
||||
-- Update label and icon_svg
|
||||
v_items := jsonb_set(v_items, ARRAY[v_idx::text, 'label'], '"充值/订阅"'::jsonb);
|
||||
v_items := jsonb_set(v_items, ARRAY[v_idx::text, 'icon_svg'], to_jsonb(v_icon));
|
||||
|
||||
UPDATE settings SET value = v_items::text WHERE key = 'custom_menu_items';
|
||||
|
||||
RAISE NOTICE '[migration-097] Fixed migrated_purchase_subscription: label=充值/订阅, icon=CreditCard SVG';
|
||||
END $$;
|
||||
@@ -1,17 +0,0 @@
|
||||
-- 098_remove_easypay_from_enabled_payment_types.sql
|
||||
--
|
||||
-- Removes "easypay" from ENABLED_PAYMENT_TYPES setting.
|
||||
-- "easypay" is a provider key, not a payment type. Valid payment types
|
||||
-- are: alipay, wxpay, alipay_direct, wxpay_direct, stripe.
|
||||
--
|
||||
-- Idempotent: safe to run multiple times.
|
||||
|
||||
UPDATE settings
|
||||
SET value = array_to_string(
|
||||
array_remove(
|
||||
string_to_array(value, ','),
|
||||
'easypay'
|
||||
), ','
|
||||
)
|
||||
WHERE key = 'ENABLED_PAYMENT_TYPES'
|
||||
AND value LIKE '%easypay%';
|
||||
@@ -1,16 +0,0 @@
|
||||
-- Add payment_mode field to payment_provider_instances
|
||||
-- Values: 'redirect' (hosted page redirect), 'api' (API call for QR/payurl), '' (default/N/A)
|
||||
ALTER TABLE payment_provider_instances ADD COLUMN IF NOT EXISTS payment_mode VARCHAR(20) NOT NULL DEFAULT '';
|
||||
|
||||
-- Migrate existing data: easypay instances with 'easypay' in supported_types → redirect mode
|
||||
-- Remove 'easypay' from supported_types and set payment_mode = 'redirect'
|
||||
UPDATE payment_provider_instances
|
||||
SET payment_mode = 'redirect',
|
||||
supported_types = TRIM(BOTH ',' FROM REPLACE(REPLACE(REPLACE(
|
||||
supported_types, 'easypay,', ''), ',easypay', ''), 'easypay', ''))
|
||||
WHERE provider_key = 'easypay' AND supported_types LIKE '%easypay%';
|
||||
|
||||
-- EasyPay instances without 'easypay' in supported_types → api mode
|
||||
UPDATE payment_provider_instances
|
||||
SET payment_mode = 'api'
|
||||
WHERE provider_key = 'easypay' AND payment_mode = '';
|
||||
@@ -1,6 +0,0 @@
|
||||
-- 100_add_out_trade_no_to_payment_orders.sql
|
||||
-- Adds out_trade_no column for external order ID used with payment providers.
|
||||
-- Allows webhook handlers to look up orders by external ID instead of embedding DB ID.
|
||||
|
||||
ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS out_trade_no VARCHAR(64) NOT NULL DEFAULT '';
|
||||
CREATE INDEX IF NOT EXISTS paymentorder_out_trade_no ON payment_orders (out_trade_no);
|
||||
@@ -1041,7 +1041,7 @@ const flushPendingAutoLoad = () => {
|
||||
const source = pendingAutoLoadSource.value
|
||||
pendingAutoLoad.value = false
|
||||
pendingAutoLoadSource.value = undefined
|
||||
loadUsage(source).catch((e) => {
|
||||
loadUsage({ source }).catch((e) => {
|
||||
console.error('Failed to load deferred usage:', e)
|
||||
})
|
||||
}
|
||||
@@ -1053,7 +1053,7 @@ const requestAutoLoad = (source?: 'passive' | 'active') => {
|
||||
pendingAutoLoadSource.value = source
|
||||
return
|
||||
}
|
||||
loadUsage(source).catch((e) => {
|
||||
loadUsage({ source }).catch((e) => {
|
||||
console.error('Failed to auto load usage:', e)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1703,9 +1703,23 @@ export default {
|
||||
title: 'OpenAI Messages Dispatch',
|
||||
allowDispatch: 'Allow /v1/messages dispatch',
|
||||
allowDispatchHint: 'When enabled, API keys in this OpenAI group can dispatch requests through /v1/messages endpoint',
|
||||
defaultModel: 'Default mapped model',
|
||||
defaultModelPlaceholder: 'e.g., gpt-4.1',
|
||||
defaultModelHint: 'When account has no model mapping configured, all request models will be mapped to this model'
|
||||
familyMappingTitle: 'Family Default Mapping',
|
||||
familyMappingHint: 'Requests that match the Opus, Sonnet, or Haiku families will prefer the target model configured here.',
|
||||
opusModel: 'Opus Target Model',
|
||||
opusModelPlaceholder: 'e.g., gpt-5.4',
|
||||
sonnetModel: 'Sonnet Target Model',
|
||||
sonnetModelPlaceholder: 'e.g., gpt-5.3-codex',
|
||||
haikuModel: 'Haiku Target Model',
|
||||
haikuModelPlaceholder: 'e.g., gpt-5.4-mini',
|
||||
exactMappingTitle: 'Exact Model Overrides',
|
||||
exactMappingHint: 'Exact Claude model overrides take priority over the family defaults and can route a specific Claude model to a different target model.',
|
||||
noExactMappings: 'No exact model overrides yet',
|
||||
addExactMapping: 'Add Exact Mapping',
|
||||
claudeModel: 'Claude Model',
|
||||
claudeModelPlaceholder: 'e.g., claude-sonnet-4-5-20250929',
|
||||
targetModel: 'Target Model',
|
||||
targetModelPlaceholder: 'e.g., gpt-5.4',
|
||||
removeExactMapping: 'Remove Exact Mapping'
|
||||
},
|
||||
invalidRequestFallback: {
|
||||
title: 'Invalid Request Fallback Group',
|
||||
|
||||
@@ -1789,9 +1789,23 @@ export default {
|
||||
title: 'OpenAI Messages 调度配置',
|
||||
allowDispatch: '允许 /v1/messages 调度',
|
||||
allowDispatchHint: '启用后,此 OpenAI 分组的 API Key 可以通过 /v1/messages 端点调度请求',
|
||||
defaultModel: '默认映射模型',
|
||||
defaultModelPlaceholder: '例如: gpt-4.1',
|
||||
defaultModelHint: '当账号未配置模型映射时,所有请求模型将映射到此模型'
|
||||
familyMappingTitle: '系列默认映射',
|
||||
familyMappingHint: '当请求命中 Opus、Sonnet、Haiku 系列时,会优先使用这里配置的目标模型。',
|
||||
opusModel: 'Opus 映射模型',
|
||||
opusModelPlaceholder: '例如: gpt-5.4',
|
||||
sonnetModel: 'Sonnet 映射模型',
|
||||
sonnetModelPlaceholder: '例如: gpt-5.3-codex',
|
||||
haikuModel: 'Haiku 映射模型',
|
||||
haikuModelPlaceholder: '例如: gpt-5.4-mini',
|
||||
exactMappingTitle: '精确模型覆盖',
|
||||
exactMappingHint: '精确 Claude 模型覆盖优先级高于系列默认映射,可将某个具体 Claude 模型单独映射到不同的目标模型。',
|
||||
noExactMappings: '暂无精确模型覆盖规则',
|
||||
addExactMapping: '添加精确映射',
|
||||
claudeModel: 'Claude 模型',
|
||||
claudeModelPlaceholder: '例如: claude-sonnet-4-5-20250929',
|
||||
targetModel: '目标模型',
|
||||
targetModelPlaceholder: '例如: gpt-5.4',
|
||||
removeExactMapping: '删除精确映射'
|
||||
},
|
||||
invalidRequestFallback: {
|
||||
title: '无效请求兜底分组',
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<div><label class="input-label">{{ t('payment.admin.validityDays') }} <span class="text-red-500">*</span></label><input v-model.number="planForm.validity_days" type="number" min="1" class="input" required /></div>
|
||||
<div><label class="input-label">{{ t('payment.admin.validityUnit') }} <span class="text-red-500">*</span></label><Select v-model="planForm.validity_unit" :options="validityUnitOptions" /></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div><label class="input-label">{{ t('payment.admin.sortOrder') }}</label><input v-model.number="planForm.sort_order" type="number" min="0" class="input" /></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('payment.admin.features') }}</label>
|
||||
<textarea v-model="planFeaturesText" rows="3" class="input" :placeholder="t('payment.admin.featuresPlaceholder')"></textarea>
|
||||
@@ -102,7 +105,7 @@ const { t } = useI18n()
|
||||
const appStore = useAppStore()
|
||||
|
||||
const saving = ref(false)
|
||||
const planForm = reactive({ name: '', group_id: null as number | null, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', for_sale: true })
|
||||
const planForm = reactive({ name: '', group_id: null as number | null, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', sort_order: 0, for_sale: true })
|
||||
const planFeaturesText = ref('')
|
||||
|
||||
const validityUnitOptions = computed(() => [
|
||||
@@ -130,10 +133,10 @@ const selectedGroupInfo = computed(() => {
|
||||
watch(() => props.show, (visible) => {
|
||||
if (!visible) return
|
||||
if (props.plan) {
|
||||
Object.assign(planForm, { name: props.plan.name, group_id: props.plan.group_id, description: props.plan.description, price: props.plan.price, original_price: props.plan.original_price || 0, validity_days: props.plan.validity_days, validity_unit: props.plan.validity_unit || 'days', for_sale: props.plan.for_sale })
|
||||
Object.assign(planForm, { name: props.plan.name, group_id: props.plan.group_id, description: props.plan.description, price: props.plan.price, original_price: props.plan.original_price || 0, validity_days: props.plan.validity_days, validity_unit: props.plan.validity_unit || 'days', sort_order: props.plan.sort_order || 0, for_sale: props.plan.for_sale })
|
||||
planFeaturesText.value = (props.plan.features || []).join('\n')
|
||||
} else {
|
||||
Object.assign(planForm, { name: '', group_id: null, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', for_sale: true })
|
||||
Object.assign(planForm, { name: '', group_id: null, description: '', price: 0, original_price: 0, validity_days: 30, validity_unit: 'days', sort_order: 0, for_sale: true })
|
||||
planFeaturesText.value = ''
|
||||
}
|
||||
})
|
||||
@@ -149,6 +152,7 @@ function buildPlanPayload() {
|
||||
original_price: planForm.original_price || 0,
|
||||
validity_days: planForm.validity_days,
|
||||
validity_unit: planForm.validity_unit,
|
||||
sort_order: planForm.sort_order,
|
||||
for_sale: planForm.for_sale,
|
||||
features,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user